Debugging Functional Programs

An interesting thread on the Haskell mailing list.

I am not sure whether the most reasonable conclusion from this thread isn't to not choose lazy languages. I don't think it's simply a problem of lack of tools. Debugging lazy code is iherently problematic.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Debugging and dynamic properties

Debugging is a way to gain insight into dynamic properties of programs, usually to find a bug in the code. Can the same objective be attained by other means ? Program analysis, maybe ?

I understand the analysis would be based on a semantic model for the language, and it couldn't account for everything that happens when the program is actually executed in a real platform.

Still, I am playing the devil's advocate here: is debugging really necessary or just a way to cope with our inability to reason (by ourselves or with tools) about programs ?

is debugging really necessary?

Personally, I never debug. I just write it correctly the first time ;-)

Joking aside, I think debuggers are only useful in very specific circumstances, and prefer other techniques for general day to day programming. However, lazy-pure languages make other techniques (e.g, debug prints) problematic as well.

avoid debugging

I too avoid using debuggers, prefering to reason about what I wrote. But a good debugger seems important to many people, and is often a pragmatic criterion in language selection.

What I thought was: what kind of tools or techniques could give enough information about the execution of programs as to make debuggers really unnecessary ?

Also, I may be wrong, but I think debuggers aren't studied very much. Is it because it's a solved problem in strict languages ? Or is it because it's not seen as a worthy research topic ?

research on debuggers

is it because it's not seen as a worthy research topic ?

I think that the reason. It seems like a practical/technincal problem, not something that requires deep research. Obviously, that's not the whole story, and some reasearch is being done. Just not enough.

I rarely ever debugged...

when I was a Java programmer. Usually I would isolate the bug with a test case, narrow it down with some tests and correct it. Also I used contracts everywhere I could.

I would say that these techniques could make debug virtually unnecessary, but without a correct stack-trace (another thing Fergus complains about) it would be much harder to get around without a debugger.

Debuggers are a language issue.

Strange, when I program in C/C++ I live in the debugger. I can do without it. When I program in ruby, I never need it. Why? Reasons I can think of are...
  1. All my ruby code is Test Driven Development, so it is easier to add a test, than use the debugger.
  2. The TDD code has far fewer bugs.
  3. The compile/link/run cycle time in ruby is zero, therefore it is easier to add a tracing print statements.
  4. gdb will print all the instance variables of an object, but c/c++ won't. Ruby has 'pp' which prints recursively dumps an object and its subobjects.
  5. Backtraces from C/C++ are only available from within the debugger, wherease I often use this idiom in ruby...
      raise "Whinge" unless assertion
    
    which gives a stacktrace when fired.
  6. gdb is an additional language tacked on the side of C/C++. Whereas ruby is very capable of introspection and reflaction. Therefore instead of having a braindead mini-language to use for debugging, you have a "best of breed" scripting language to script your debugging in.

    ie. There is a hard division between the debugging language and the programming language in C/C++/gdb world. In the ruby world ruby _is_ the language of my debugger.

When you can't reason, debug

is debugging really necessary or just a way to cope with our inability to reason (by ourselves or with tools) about programs ?

I think there is a lot of truth to that statement. I try to write my programs so that if I am examining about a page of the source code at once, there is enough information contained therein for me to understand what is going on. However, that's not always possible and eventually you get lost.

In general, I don't have problems reasoning about control flow, but rather it's data transformation and/or updating that it's easy to lose track of. I often ask myself "What is the value of variable x at this point?", etc. When that gets too much for me to keep track of and something goes wrong, I may feel the need to start up a debugger.

contracts!

The thread makes me think of contract systems. We're talking about very large programs that fail with errors like "I can't take the head of an empty list! I give up." With contracts for HO functions, you get "module A is guilty of violating contract X, which caused module B to take the head of an empty list." Which would be much more useful for debugging a large program, I should think.

Or not

Which would be much more useful for debugging a large program, I should think.

I am big on contract, as I guess everyone knows. But I have to disagree on this one. Sure, contracts help you assign balme. But then you have a module that breaks and you want to know why. More specifically, you want to know which statement in the code is responsible: where's the bug. And for this people use debuggers, and this is exactly what contracts don't give you.

Obviously, this does not mean contract aren't important, or that I like debuggers...

Re: or not

More specifically, you want to know which statement in the code is responsible...

Well, depending on the granularity of the contract system, you can get close. In Robby Findler's calculus you can associate contracts with individual definitions, so that you can trace blame back to specific functions.

But I think you're right: Robby has indicated in emails that he thinks definition-level contracts are a bad idea and they might remove them from PLT Scheme. I think his and Matthias Felleisen's view is that contracts are more useful for inter-module relationships (e.g., to coordinate efforts between multiple programmers), and that if you want intra-module guarantees you should use something else, like types.

I get the impression that a big practical impediment to intra-module contracts is that they break proper tail-recursion. Steering back towards the topic at hand, this is a problem with stack traces as well, as someone in the thread pointed out. What do people think about the relationship between proper tail recursion and debugging?

It seems certain enforcements imposed by a language for the sake of safety or reasoning can impede the kinds of experimentations we might want to do in development and debugging. Are there useful and perhaps general ways to characterize a distinction between development and production versions of programs? I can think of a number of features you'd like to ease development that you would not want enabled in production, e.g.:

  • disabling TCO for full stack traces
  • various program instrumentation
  • test scaffolding
  • adding side-effects to pure code (e.g., lifting pure Haskell functions in the IO monad)

Maybe these are all in the category of "dirty hacks" (which is not Cartesian-closed, I hear). But I can imagine a distinction being useful for AOP.

I think his and Matthias Fel

I think his and Matthias Felleisen's view is that contracts are more useful for inter-module relationships

That's essentially my view as well. There are some exceptions (e.g., inheritance hierarchies are problematic whether inter- or intra- module) but a contract is (a part of) an interface.

Omniscient debugging

I'm not a Haskell expert but is there any reason (besides performance) that makes an Omniscient Debugger impossible? We could have some defaults points of interest (e.g. list consing, ">>=", exceptions) that would be logged when the thunk is evaluated.

According to the creator it took less than a month to write it in Java: "Not only possible, but amazingly simple. I wrote the first line of code on January 6th, and it was working on February 6th! And that includes buying Lindholm & Yellin ("The Java Virtual Machine") and writing my first Java byte code!". Therefore writing one in Haskell would take much less time ;-)

Oh my, is this ever interesting.

This would be perfect for our internal language.

Thank you for the link Daniel.

Type errors

I can't help but grin, just a little bit, to see Haskell programmers having so much difficulty with type errors.

Is this the difference between theory and practice, perhaps just for certain applications?

Which were those?

I think the difficulty was lazy evaluation, not type errors, right? I'd be tempted to agree that laziness hasn't worked out as well in practice as might have been hoped.

Lazyness is hard

Right, I think that's the core problem.

Top Haskellites agree

I'd be tempted to agree that laziness hasn't worked out as well in practice as might have been hoped.

Simon Peyton Jones addressed this in his
Haskell restrospective, concluding that although laziness has some attractive properties, it also has problems, and that overall, laziness doesn't "really matter". He says "purity is more important than, and quite independent of, laziness".

Both?

Lazy evaluation is making debugging hard, but he's trying to debug a type error, right? (Taking the head of the empty list.)

Oh, I see

How is that a "type error" though? Did you mean that you thought it was funny that the type system didn't save him from making such a simple runtime error as taking the head of an empty list? That's an interesting point. I think this may be one of those "human factors" things again - it's so convenient to use lists in Haskell, that one often uses a list where one really should be using a different data type that can't be "empty".

It's a runtime error

...as Bryn says. Otherwise there would be no need for a debugger; Haskell catches type errors statically. Though I can conceive of a static debugger for static type system, for example:

D. Duggan and Frederick Bent.  Explaining type inference.  Science of Computer Programming, 27,1, June 1996.

Like Bryn said, this error arises precisely because Fergus bypassed the type system. Personally, I never use head, and I'm very careful about non-exhaustive patterns. Often when you have the latter, it means you're using the wrong datatype.

runtime errors that are type errors

From time to time the question is asked on the Haskell lsits, "Shouldn't the compiler detect incomplete patterns?" If the compiler reported these as errors, many if not most runtime errors would be eliminated, because functions like 'head' would be required to handle the "empty list" case explicitly.

Because a more rigorous type system would catch them, they can be viewed as type errors.

Non-exhaustive patterns

The compiler does detect non-exhaustive patterns; in fact, it emits a warning when it finds one.

You don't need a more expressive type system to catch errors like that with head; all you need is a programmer who is willing to use the tools the language offers him.

It isn't a matter of type system

As we always can write:


head (x:_) = x
head [] = error "head []"

Where error is polymorphic, so it type-checks and the problem still exists. In the general case the type-system can't forbid an error-like polymorphic expression.

Saying that I agree that incomplete patterns are a source of errors and I don't like the error function as it leads to sloppy error handling in the program.