When faced with working programming assignments, many students, not having had the benefit of a lot of experience, resort to methods that look to them like shortcuts, but in reality lead into a swamp. They spend a staggeringly long time trying to slog through the swamp until they finally give up. Then, for the next assignment, many head out into the swamp again, with clean boots but no better a chance of success.
For those who want to avoid the swamp, here is how to do it.
The programming assignments are designed to teach fundamental concepts.
If you don't bother to read the assignment, or if you try to avoid doing what the assignment tells you to do, you will receive little or no credit.
Follow the assignment.
If you start early, you will probably be willing to try the methods discussed below, and you will develop your software efficiently.
If you start late, you will probably decide that you do not have time to do those things, and that you have no choice but to try to run across the swamp, hoping not to get stuck in the mud. That won't work. The mud will get you. Then the swamp weasels will come.
A certain faculty member who did not believe in documentation spent a long time writing a piece of software. Then he had to put it down for a few months over the summer. When he came back to it, he said that he could not understand any of it. He could not even reverse-engineer it. He had no choice but to throw it away and start over.
A common question I get asked is: "why doesn't my function work?"
Naturally, I ask what the function is intended to do. I can't help much without knowing that. The most frequent answer that I get is a description of the body of the function, line by line. But if that is what it is supposed to do, then it obviously works! The issue is, what do you want it to do?
Before you can write a definition of a function, you obviously need to know what the function is intended to do. If you know that, you should be able to write it down. So write it down.
Without contracts, you will find yourself spending time reverse-engineering what you wrote earlier. You will be surprised how much it helps to have clear, precise and correct contracts.
Before you write any code, work out algorithms by trying them on examples. If your code uses data structures, draw pictures of those data structures. Trying to keep everything in your head is a huge mistake.
Walk through your algorithm on examples and see if it appears to work.
When you are working out an algorithm, you often find yourself thinking "it would be nice if I had a function that did …" Top-down design tells you to work as if you have such a function. Give it a name and write a contract for it.
When you are done with the current function, you will need to find or create the helper function(s). If you can find the helper function in a library, great. But if you can't, that is not a problem. Just write it!
The key points of top-down design are:
Remember that you do not need to implement every function in the most primitive form. It is often easier to write a function using some helper functions. How would you write a function to find the smallest of 3 integers?
Make your helper functions sensible and as general as you can without a lot of extra work. For example, if you need to swap two variables in an array, don't write a function to swap two variables in an array. Write a function that swaps any two variables.
Sometimes, you will find that you can use your more general functions elsewhere. But even if you don't, using general tools tends to lead to code that is easier to understand.
When you realize that you want a helper function, don't stop what you are doing and go off and write a definition of the helper function. You will lose your train of thought about the function that you are working on. You can write the helper function later.
Guessing is an acceptable way to work on a problem as long as
If you think of an idea for an algorithm that might work, check it out carefully. At a minimum, try it on some examples. Avoid confirmation bias; that is, don't coddle your algorithm by only looking at examples that are easy for it.
Guessing is not a way to fix a broken program. See below.
Students typically imagine that the best way to solve a programming assignment is to write the entire thing and then to begin testing it. But doing that takes you into the heart of the swamp.
There will inevitably be many errors. It will be difficult to determine where in your program each error occurs. Sometimes, two errors work together to make it difficult to find either one.
To save yourself lots of time wandering around in the swamp, write your code in small increments. After each increment, test what you have, and don't move on until what you have works.
The most important thing about this successive refinement is that, when discover a mistake, it is usually in the part that you have just added. So you can localize your search for the error without much thought at all.
An increment should be something that is testable. Sometimes, that involves writing two or three functions. But keep increments as small as you reasonably can.
When you have a tentative function definition, try running it by hand on a small example. Be critical. If it does not work, you want to find out.
A very common trap is to hand-simulate the code that you wish you had written rather that what you actually wrote. If you find yourself doing a hand simulation without consulting your code, you have certainly fallen into that trap.
Novices almost always test too little.
Experts know that it usually takes several tests to find mistakes. Production software undergoes thousands of tests.
You can't afford to do that level of testing for this course, but you can certainly try 3 to 5 tests.
Always test boundary values. Those are inputs that have some extreme nature to them. For example, if the input is a nonnegative integer, be sure to test 0 (the smallest nonnegative integer).
Your doctor always diagnoses your illness before trying to cure it. You are your software's doctor. If something is wrong with it, find out what is wrong before you try to fix the problem.
Especially when faced with a time crunch, students often start playing the lottery. Their reasoning goes like this: "I don't really want to understand what the problem is. I just want to fix it." So they try a random guess, changing something that might fix the problem.
Of course, all that does is move the software farther away from being right. It eats up time that you could have used to diagnose the problem, and you end up having destroyed anything that was correct. Find a way to keep yourself from doing that. It is a waste of time.
Do a specific diagnosis. Which function is broken? In what way is it broken? More often than not, once you have that information, it is straightforward do see how to fix the error.
If you are having trouble diagnosing a problem, use traces. Make the program write out what is happening.
Never show raw data in a trace. Always say who's talking (the function where the trace is) and clearly label information that is shown. For example,
printf("spotter: Trying to find %d\n", toFind);shows who is talking (spotter) and what it is trying to do.
Time spent making traces readable is well spent.
If there is important information, show it. Show parameters and results. Clearly label them. A trace such as
printf("I am here");is worse than useless. (When I see that, I am reminded of a person I once knew who wrote "taken last thursday" on the backs of photographs.)
A debugger can be a useful tool in certain circumstances. It can quickly show you where any of the following are happening.
A debugger can also be employed for other errors, but be careful about that. It is tempting to single-step through the program. But the error can occur millions of steps in.
For more subtle problems, tracing is your best tool.