10. Week 6 Tuesday: Revisiting Wordle with Functions
≪ 9. Week 5: Making a Wordle Clone | Table of Contents | 11. Week 6 Thursday: References ≫Functions and the Box Model
A little while ago, we introduced the box model for C++ as a way to represent the state of the program. What’s important is that it’s one guy (Cecil, Mark, Steve) and a bunch of boxes in a warehouse moving data around.
Let’s imagine that we are the guy working at the warehouse. When creating complicated projects such as last week’s Wordle, the list of instructions we would have to perform can get pretty out of hand. Our life would be much easier if we were promoted to a managerial role, dispatching an army of underpaid independent contractors to do all the hard work with the boxes instead of doing it ourselves. This is the principle behind functions.
Each function or “job” must describe three key components:
- A possibly empty list of “input data”, called the parameters.
The function
make_coffeemay require the amount of sweetener, creamer, and type of bean to even be executed. - A set of instructions, called the “function body”.
The function
make_coffeewould have instructions on how to brew the coffee, etc. - A return type — this is what you expect the contractor
to give you once the job has been done.
The function
make_coffeeshould return a cup of coffee, for instance, but each cup may be different depending on the parameters. Note that this can bevoid, where you expect the contractor to silently finish their job and never speak with you again (e.g., the job is cleaning a bathroom… there’s nothing to return!)
You can already see this at work in some problems we’ve done!
The max function in C++, for instance,
takes two integers as parameters,
has a mysterious function body that determines the bigger one,
and “returns” the larger of the two.
You can imagine giving a toddler two numbers,
watching it (them?) derp around for a while,
and toddling back to you with the bigger number.
To illustrate this analogy, let’s consider the following example:
Problem 1.
Determine the output of the following code.
1#include <iostream>
2#include <string>
3
4using namespace std;
5
6string f(string input) {
7 string s = "";
8 for(int i = input.length() - 1; i >= 0; i -= 2) {
9 s += input.at(i);
10 }
11
12 return s;
13}
14
15int main() {
16 string s1 = "pic10a rox";
17 string s2 = "oatmeal sux";
18
19 string s3 = f(s1);
20 cout << s3 << endl;
21 cout << f(s2) << endl;
22 return 0;
23}
The function f is describing the following job:
- The independent contractor receives a string called
input. We can model this as giving the contractor a box labelledinputand containing a string. - The contractor is to then extract every other character
from the string
input(think about this!), starting at the end and going backwards, and stores it in the strings. - When they are done,
they “return” to us with this string
s, which we can then store in a box ourselves or print to the screen!
The program is still the set of instructions in the main function,
and the first two lines say to create two string variables
s1 and s2 with whatever contents.
On line 19, which says string s3 = f(s1);,
we first get really lazy and hire a contractor (maybe named Bob)
to perform the job f described above.
We create a box called input,
copy the contents of s1 into it, hand it off to Bob,
and tell him to do his job.
Bob then diligently extracts every other character and such,
producing the string xra1i.
He then “returns” to us with this string,
allowing us to store xra1i in the string variable s1.
Now Bob’s job is done.
All the boxes he used —
the string input, the string s, and the int i (in the loop) —
are destroyed in the process.
Bob is fired and is never seen again.
On line 20, we print the contents of s1, which is xra1i.
On line 21, we need to run job f again,
and we hire a new guy (maybe Joe)!
We still need to provide him a string called input,
but this time we copy the contents of s2 into it.
We tell Joe to do his job,
and he “returns” to us with the string xsleto.
We directly print this string to the screen.
I hope this analogy with manual labour and independent contractors is not too contrived for your taste. I think one benefit is that it allows us to rationalise many of the common mistakes that are made with functions. Let’s consider the following motivating example:
1// accepts a string as an input.
2void has_digit(string s) {
3 // for each character in the string,
4 // check if that char. is between '0' and '9'.
5 // if yes, return true!
6 for(int i = 0; i < s.length(); i++) {
7 if('0' <= s.at(i) && s.at(i) <= '9') {
8 return true;
9 }
10 }
11}
12
13int main() {
14 cout << "Enter a word: ";
15 string input;
16 cin >> input;
17
18 if(has_digit(input)) {
19 cout << "Your input had a digit in it!" << endl;
20 } else {
21 cout << "Mathematicians around the globe hate you."
22 << endl;
23 }
24
25 return 0;
26}
Here, there are two immediate flaws that appear:
- The function
has_digithas return typevoid, i.e. we expect the contractor executing this job to never come back to us. Yet in the function body, there is a command that says toreturn true;… this produces a build error. We need to change the return type to abool! - Suppose the string
sdoesn’t have any digits at all. What will the contractor return to us with? Assuming that the previous fix has been implemented, we’ll be waiting for the contractor to come back with abooluntil the end of time… The compiler is smart enough to recognise that this is an issue! We need to ensure that the contractor returnsfalseif they cannot find a digit anywhere.
Functions in Practise
Let’s see how functions are written in practise by revisiting our Wordle clone from last week. Our first iteration of pseudocode was pretty straightforward:
- Pick a random five-letter word.
- Repeat 6 times:
- Ask the user for a guess.
- Verify that the user has guessed a real 5-letter word.
- Show which letters are incorrect, partially correct, and correct.
- If all five letters are correct, the user wins!
- If the user used all six guesses and hasn’t won, then the user has lost.
Some of these pieces of pseudocode are relatively easy to translate into C++ code directly, such as asking the user for a guess. But other steps, such as picking a random word and validating the guesses, took a substantial amount of effort. We ended up needing a second layer of pseudocode, and the resulting C++ program no longer closely lines up with the above outline very closely. It is not hard to imagine that even more complex programs quickly become unmanageable if we keep everything in one big main function.
Let us now demonstrate the practical benefit of functions by reorganising our Wordle code to incorporate them. The rule of thumb I’ll follow is, whenever there’s a complex verb that isn’t easily coded, I’ll just design a function that accomplishes the job. As such, there are three spots that demand a function:
- Picking a random word.
- Validating a user’s guess.
- Finding the correct, incorrect, and partially correct letters.
Let’s begin with picking a random word.
I imagine the main program tells a servant,
“Fetch me a random word.”
We can name the function getRandomWord
(we are describing the job).
The servant shouldn’t require anything to get a random word,
so there should be no parameters for the function.
And finally, we expect the servant to come back with a word,
i.e. with a string, so that’s our return type.
We had already written code for actually getting the random word,
so here’s the function put together:
1string getRandomWord() {
2 const string WORD_LIST = getWordList();
3 int numWords = WORD_LIST.length() / 6;
4 int n = (rand() % numWords) + 1;
5 string word = WORD_LIST.substr(6 * n - 6, 5);
6 return word;
7}
This is essentially copy-pasted from our existing program,
but with two changes:
first, we had to define WORD_LIST in this function
since this function (our servant) doesn’t have access
to the main program’s boxes.
Second, we had to add the extra command return word
so that the random word actually makes it back to the main program!
In the main program, we just replace that chunk of code with
Notice how clear the code becomes!
Next, let’s write a function that validates the user’s guess.
I’ll call it validateWord.
This now requires a string parameter containing the user’s guess
(I have to tell our servant what word they’re validating),
and I expect them to come back with a true or false answer,
i.e. it should have a return type of bool.
1bool validateWord(string guess) {
2 const string WORD_LIST = getWordList();
3 bool invalid = (guess.length() != 5) ||
4 (WORD_LIST.find(guess) == string::npos);
5 return !invalid;
6}
This returns true if the word is valid and false otherwise.
The effect on the length of our code
in the main program is negligible,
but notice the difference in clarity:
1if(!validateGuess(guess)) {
2 // print an error message
3 cout << "Invalid guess! Try again. " << endl;
4
5 // update the for loop counter and repeat this iteration
6 i--;
7 continue;
8}
Problem 2.
Design and fill out a function that handles the correct, incorrect, and partially correct letters.