Item 40: Don t debug too much at once.


Item 40: Don't debug too much at once.

The most fundamental problem I see with students trying to fix misbehaving Perl code is one that has nothing to do with Perl at all. That problem is a lack of debugging strategy. Good debuggers (as in, programmers who are good debuggers ) are made, not born. Some people seem to have a particular knack for debugging, but more often than not they are "coincidentally" very experienced programmers who learned to program in adverse environments. I've personally performed a number of feats of debugging wizardry in my time, including debugging by telephone programs I didn't write and had never seen before. There is nothing magical about this, though. I just happen to know what I am doing, just as other good debuggers do.

Good debuggers have a good debugging strategy. Such a strategy is independent of language. A good debugging strategy is something that Perl programmers can make use of just as much as programmers in other languages can. Here are three rules that should point you in the right direction. They aren't a perfect or complete strategy, but if you follow them thoughtfully, you should find that debugging Perl isn't that formidable a chore after all.

Test your code as often as possible

Another way of saying this is: Write a little bit at a time .

If you are new to Perl, or are trying out features that you aren't sure of, don't type a 300-line program into your text editor and then try running it for the first time. It won't run, you will get screens full of bewildering error messages, and you will never make any meaningful progress toward getting it to work. Instead, find a way to write your program a little piece at a time.

What an appropriately sized piece of code is depends on your skill level, your familiarity with the task and the language, and how hard it is to run the program while work is "in progress."

Anecdote: I once spent several days rewriting a C++ collection class library from templates into macros (this is reverse progress, I know). The library formed the innards of a 25,000-line program, and there was no way to test it until I was done. Unbelievably, it worked the first time I was finally able to compile it and "flip the switch," and my hundreds of lines of mostly manual changes didn't introduce any bugs! In shock , I took the rest of the day off. In the real world, though, this almost never happens. A few hours of work usually means a few new bugs .

Perl is different from compiled languages like C and C++ in that there is no compile-link-run cycle to slow your testing down. If you are making incremental changes to a program, you should try running and testing it as often as possible. Because a Perl program generally starts running faster than you can type the command that starts it, there is very little cost to incremental testing. I usually try running a program every few minutes as I am writing it, and I try to write it in such a way that it will be at least partially functional throughout its development.

Even if you can't add small increments of functionality to your program, you can try testing small pieces of the code that you're adding. Use the Perl debugger to try out snippets (see Item 39) or write and run small programs that verify your new additions. It's a whole lot easier to put together a bunch of working parts than it is to fix a big broken mess.

Find out where and why your program is failing

This may seem incredibly obvious, but you cannot hope to begin debugging a program of significant length without knowing exactly where it is failing. Perl helps you out by printing out the line number and filename of the file where a fatal run-time error occurred. However, the error that causes Perl to hiccup isn't necessarily the error that you are looking for. Consider a program containing code like the following:

A program to read a hash of configuration values from a text file.

 open F, $config_file;  while (<F>) {    ($k, $v) = split;    $config{$k} = $v;  } 

Read a line.

Split out key and value.

Save them in hash %config .

Then, farther down:

 $x_real =    $x / $config{X_SCALE}; 

Divide by zero error?

This code could very well die with a divide-by-zero error at the line indicated, but the real problem is that the code lacks some important sanity checks. For one, the return status of the open at the top of the example is ignored. If $config_file does not exist, %config will contain nothing, and all of its values will appear to be undef . A proper open would look more like:

 open F, $config_file or    die "couldn't open $config_file: $!" 

It would also be wise to verify the validity of the data read:

 die "X_SCALE must be specified" unless exists $config{X_SCALE};  die "X_SCALE must be nonzero" unless $config{X_SCALE}; 

Bugs can be much stranger than this. I once saw a program that had something like the following in it:

 use FileHandle; 
 
 sub file_func {    my $fh = shift;    # some stuff using $fh here ...    return *fh;  } 
 
 open FH, "somefile.txt" or    die "couldn't open: $!"; 
 
 my $fh = file_func(\*FH); 

At this point, $fh doesn't work what happened ?

This program was written by someone who was trying to tackle my , typeglobs, and references all at once, and who apparently broke the "test your code as often as possible" rule. His code was actually much more involved than this somewhat purified example, and the problems with it were not obvious at first.

When I got hold of the code, the first thing I noticed were the two typeglobs. Passing filehandles by globref ( \*FH ) is sanctioned (see Item 26), but returning a typeglob is unusual. The interior of file_func was working properlyyou can use filehandle globrefs just like filehandlesbut the return value of file_func was apparently not a valid filehandle. Hmm. Well, in looking at the code I couldn't tell offhand exactly what sort of value it might be. I resorted to Data::Dumper and found that the value returned from file_func was indeed a globref to a variable named $fh , but $fh was undefined. The code obviously wasn't right but what I was seeing still didn't make any sense. After a bit of reflection it occurred to me that the *fh glob was referring to the global variable $fh , not the my $fh inside file_func . (Remember that my variables don't appear in the symbol table? Item 23it's easy to forget.) Perl is tolerant of all kinds of abuses of typeglobs, so there was never a run-time error that had anything to do with the actual problem.

I patched things up with the suggestion that the programmer change the return *fh to return $fh , but consider other ways of writing the program perhaps using FileHandle .

Never take anything for granted

Every once in a while a bug comes along that takes an unusually long time to exterminate. If you've been around a while you are probably familiar with the type. You may be used to finding and fixing most bugs in minutes and almost all the rest in an hour or two, but once in a blue moon you encounter a bug that is just resistant . These are the kinds that take hours, or even days, to fix. Often, you can't even figure out what's going wrong or where the source of the error is. For experienced programmers, most such bugs are the result of a basic human failing that has little to do with programming: overlooking the obvious .

I can't provide a list of "obvious" things that you shouldn't overlook, but I can provide a couple of embarrassing examples to illustrate what I'm talking about. Not that long ago, I was trying out some snippets of Perl in the debugger. I was using several different versions of Perl and, in one of them, I pasted something like:

 %x = { foo => 1, bar => 2, baz => 3 }; 

It accepted the code without comment. I pasted the same code into the debugger in a later (development) version of Perl, and it emitted a page of gobbledygook that started out with " Odd number of elements in hash list at (eval 3) line 2 ". The page of gobbledygook struck me as a debugger problem, because most of it was nonsensical . I became fixated on this and spun my wheels for a while. It took me about an hour to notice that in addition to the debugger problem, which was real, I had also substituted braces for parentheseswhich was what the very first line of the error message was telling me in a roundabout way. I should have written:

 %x = ( foo => 1, bar => 2, baz => 3 ); 

Another more clear-cut case of overlooking the obvious comes to mind. I was trying to fix a bug in some page-layout routines in a large C++ application (the one mentioned in the Preface). It had taken me a long time to pin down the source of the bug and a while longer still to concoct a fix. I began to code the fix. When I was partially done, I did a test compile, ran the program, and noticed that I was making progress. With a sense of relief, I got up from my desk and wandered off to get a soda and take a break. When I returned, I coded some more of the fix, did another test compile, and ran the program again. Now, suddenly, the program was broken. In fact, the changes I had made earlier, before getting up, didn't seem to be there.

I went back to the source code, which was already open in another window. My changes were still visible in the editor. Very strange . I wondered if the second set of changes I had made were causing a failure with symptoms like those of the original bug. I began to sprinkle all kinds of debugging statements and sanity checks throughout the source code. They didn't seem to have any effect. In fact, they didn't even seem to be showing up in the compiled program!

Around this time, I began to have fanciful thoughts about corrupted file-systems, broken NFS servers, and whatnot. Something totally weird was happening that was preventing the changes I was saving and compiling from being realized in the executable.

After about three hours of wild speculation and increasing wonderment, I found the problem. The version of the program I was running wasn't the one I was editing and compiling. For some reason, after I had come back from my break, I had started typing my make and run commands into a shell window that had a different PATH environment than what I was used to. I had changed it in that window earlier in the day and had forgotten about it. The changes I was making were being compiled into an executable , but I wasn't running that executable!

Don't overlook the obvious.



Effective Perl Programming. Writing Better Programs with Perl
Effective Perl Programming: Writing Better Programs with Perl
ISBN: 0201419750
EAN: 2147483647
Year: 1996
Pages: 116

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net