One of the complaints I occasionally hear from students is that debugging is not taught. I plead guilty to that. Pick up a book on programming, and it seems like debugging is left as an afterthought. There are now a few books on industrial-strength debugging, but they are usually meant for debugging code in industry settings (i.e., millions of lines of code, dedicated testers, software/databases to catalog bugs, infrastructure set up to do debugging). Ask an instructor why debugging isn't taught, and the answer you're likely to hear is: "Debugging can't be taught---you have to learn it yourself". I'm not sure I entirely agree with this. I think you *can* teach debugging, but that we, as teachers, haven't sat down to think about how to teach debugging. It's far easier to talk about Dijkstra's algorithm then to teach debugging. And it's far easier to get students to learn about Dijkstra's than to learn hot to debug. To begin, it's useful to know why bugs exist. 1. Syntax errors Usually, syntax errors are the easiest to remove. The compiler flags down such errors. Unfortunately, the compiler can be rather cryptic with its error messages. One lesson you learn soon enough is that sometimes the compiler flags a line on one line, but the error occurs somewhere earlier on. 2. Logic errors Your code compilers, but you did something silly like use && instead of ||. Or the code simply doesn't do what you want it to do (for example, you wish to remove an element from an array---but a simple trace shows that it doesn't do it). I'll explain ways to track logic errors down, but alas, this is a major family of errors. One could say that any error w to Debug ============ When you write code, you need an exact picture of what your code should do. That is, you need to know: 1) which lines of code are *supposed* to be executed, i.e. -- whether you are going into an "if" or into an "else" -- how many times the loop is iterating -- when conditions are true, when they are false 2) what values the variables should have 3) what the basic data structure looks like, at various steps Recently, I talked to a 214 student, who was trying to compare "HugeInts". This was a class used to store ``huge ints'' (ints larger than about 10 digits). It's implemented using a linked list, where each element stores a single digit. She was trying to handle the overloaded < (less-than operator), and was comparing the value of 3 vs. the value of 4. All she knew was that it was producing the wrong answer. That is wasn't saying 3 < 4. To her credit, she had a small example that was failing, so in principle, it should have been easy to trace (not too many lines of code). But I didn't see any pictures drawn, nor did it appear that she had traced the code. It seemed like she believed the logic had to work, so there wasn't a real need to trace it. It turned out that her logic was messed up. A simple trace would have shown that error. Of course, it would have taken more time to fix that error, because she appeared to have conceptual difficulties with the algorithm. But at least, she should have found the error, and wondered how to go about fixing the error, and she wasn't even at that stage. This anecdote leads to several important points about debugging. * If you can't explain, in pictures, what your code is doing to someone else, then more than likely, it isn't doing what it should. That person should be able to give you several inputs, and you should be able to draw pictures, and follow an algorithm to show how your algorithm produces the correct answer. Thus, the first lesson is: understand your algorithm, and make sure it works correctly (at least, for the cases you want to test). Be able to describe it to someone in pictures. * Then, you need to show that person that the code actually does what your "algorithm" says it does. Again, simple examples work best. Show which lines of code are being executed, which conditions are true, etc. Surprisingly, people really hate this part. They're convinced that tracing code involves thousands of lines. They desperately want to skip this step. For the 3 < 4 HugeInt example above, I'm sure the number of lines of code required to trace the code was probably 30 lines of code, tops. It really wouldn't have taken that long to trace. This is the first place that coding can create problems: i.e., translation from algorithm to code. Even if you have the correct algorithm (and that's already a big step), you may have failed to translate it to code correctly. This is why I think perhaps we should teach students how to use a debugger. A debugger might not help you get the algorithm to code step correct, but at least it shows you what lines of code are executed and in what order, so you can SEE what your code is actually doing, as opposed to what you THINK it's doing. Often that helps you figure out where the bug is. Of course, in order for the debugger to help, you have to know where YOU think it's SUPPOSED to go. If you have no idea what the control flow of your program is, well, tough luck trying to debug the code. (The funniest story I heard about this was a student who said he had written several ``versions'' of a program, and was wondering which was correct. I was thinking ``just find the error---this isn't some frickin' genetic algorithm where you create permutations of your program and see which one succeeds''...hmm, I seem to be fixated on the word frickin' a lot, lately...stupid Goldmember movie!) If you don't use the debugger, then (1) Write print statements that "track" where the code should be going, for a SPECIFIC INPUT. You should know what values the variables have, how many times a loop iterates, which branch of a conditional statement is being taken FOR THAT SPECIFIC INPUT. As you can see, I believe in testing with specific inputs, espcially simple ones. You'd be surprised how many algorithms I've seen coded up which don't work for any input whatsoever. One single trace with one simple input would have made this problem clear. (2) Hopefully, as you trace code (by hand or using debugging statements or preferably both), you will see that the code is not doing something properly, and you will investigate further. For example, you thought the control-flow should have entered an if-statement, but instead, it went into an else-statement. So, you think ``Why did that happen?'', and check the condition of the if-statement, and print out the values to find out why it wasn't true. If necessary, you start declaring boolean variables and break up the condition. For example: if ( num > 23 && foo( x, y ) != -1 ) can be written as: bool cond1 = num > 23 ; bool cond2 = foo( x, y ) != -1 ; if ( cond1 && cond2 ) That way, you can see if the conditions are what you expect by printing out cond1 and cond2. (3) Beware the Heisenberg Uncertainty Principle! The Heisenberg Uncertainty Principle states that you can not measure momentum and position infinitely precisely. The more accurately you measure position, the more uncertain momentum is. It's said that this is the reason why electrons stay in orbit. If the electrons radiated energy and fell to the nucleus, then its position would be known, thus, by this principle, it would gain momentum, thus keeping it in orbit. People often rephrase this as "you can't observe something without disturbing it" (which isn't necessarily true, but it's an interesting thought anyway). The code version of this is: you can't debug code without (potentially) affecting the way the code runs. In general, this is false. You can generally debug without affecting the correctness of a program. However, a debug statement/function can occasionally have a SIDE EFFECT, thus affecting what you're trying to test. Let's see this in action // debugging code here bool cond = stack.pop() ; // returns false if empty cout << cond << endl ; // check value // real code here if ( stack.pop() ) // actual if statement The debugging code checks to see if the stack is empty, but it has a SIDE EFFECT. It attempts to pop an element off the stack. If it were NOT empty, the debugging statement would pop it off once, then the if-statement would pop it off a SECOND time. Thus, testing the stack actually ALTERED the stack. Clearly, you don't want testing to make such changes. Be careful when you check conditions. Implement them as non side-effecting (i.e., doesn't modify the object), so that you can check the condition as many times as you want. (4) Know your APIs Java has lots of libraries. It's easy to use them. You SHOULD use them. But, read the frickin' APIs (with lasers). Remember, when all else fails, read the APIs (the APIs, is the application programmer's interface, and is a list of methods, and what they do). It's like reading the directions. They should, in principle, tell you how the methods behave. Read them so you don't say "well, I ASSUMED they behaved this way (stupid, frickin' Java---sugar, sugar, SUGAR!)". (5) Make hypotheses! Question everything! Don't trust your code! Because I've programmed a while, there are a few things I rarely question. I don't question the addition or assignment of integers. I don't believe assigning pointers to pointers cause core dumps. Other than that, I question everything. I question whether a function REALLY did compute the value it should have. I question whether the loop really did go 10 times or not. I question whether the array has the values that it should. I question whether the values being sent to the function were correct just PRIOR to calling the function, then I check the value just inside the function, then I check the return value just before I leave, then I check the value at the function call to see if it got assigned properly. If this had been C++, I'd definitely check and double-check assignment statements, copy constructors, destructors. I also make hypotheses. For example, I say ``I think maybe the copy constructor is buggy''. I read the code, I try to trace a few examples (mentally or by using debug statements), think of a few common errors, and possibly write some test drivers. Once I've convinced myself it works, I look at something else. At times, I will go back and question the copy constructor again. The key often is: don't stare at code, print something out and make sure it's doing what it's supposed to. (6) Write functions that help debug for you! Learn about assert statements. Write functions that check to see if your data structures have been put together correctly. Use those functions when you add and delete. (Of course, these functions can ALSO be buggy, so be careful). Again, this assumes you know which properties to test for, and can write code to check for this. (7) Learn about unit testing Unit testing is basically writing test drivers, comparing the expected result of a method to its actual result. It prints an error message (or similar) when errors occur. There's something called JUnit which provides some scaffolding to allow you to do this. It's best used when you intend to work on code for a while, and add new features. It's useful to make sure bug fixes and such don't introduce new bugs. (This is related to (7)). (8) Avoid making changes just to make changes. The most common thing I've seen in student's code that bugs me (no pun intended) is making the code convoluted and weird, just because the simple answer wasn't working. I understand the thinking behind it. If your code's not working, there are two steps to take. (1) Figure out what went wrong. (2) Solve it some other way. Many students opt for ``solve it some other way''. I say that's the wrong approach. If you can all help it, try to figure out why your code does the wrong thing. If you finally decide you can't, create a copy of the code, place it in some well-named file, describe the error in a comment within the file, and only *then* should you solve it some other way. The reason I don't like the ``solve it some other way'' approach is because it invariably leads to messy complex code that's harder to debug in the future. I believe in nice and simple code to get a task done. Nice, simple code is easier to debug and convince yourself it really does work. Complex code, while it may work, doesn't have this nice coding property. Believe me, I understand why programmers take this approach. It makes progress. Looking for why something went wrong is slow. It's easier to just keep on coding. (9) Learn more about how to write code and test. Someone's always coming up with new ideas (such as unit testing, design patterns, guerilla tactics, etc) for better code writing and testing. The more you know about how experience programmers do it, and more importantly, the more you're WILLING to learn how they do it, the better a coder you can be. If I had to boil debugging to a few sentences it would be: know what your algorithm is supposed to do, then draw pictures, and trace the code (including debugging statements) to see that the code matches with your algorithm. That is all---ask Brian how he does it =). -- Charles Lin clin@cs.umd.edu that isn't a syntax error falls in the logic error. 3. You assume a certain function behaves a certain way. This happens a lot more than you would think, esp. with libraries. Someone provides you a library, and describes what the functions do, but you, in your haste, skip over the description of the API, and begin coding, assuming it behaves a certain way. This assumption tends to hurt a lot, because often, you don't even question the assumptions. You're so sure you're right you question other things instead. 4. The documented API is buggy. This one hurts worse, because it's not even your fault. The API says one thing, but you can generate tests to show it isn't behaving as described. Sometimes that's due to poor English (on their part), sometimes that's due to your failure to understand the terminology used.