Liam O'Connor
University of New South Wales

About | Articles | Reading | Projects | Résumé

Safe
3 April 2010

This article is a short piece advocating the use of Haskell based on the strength of its testing, debugging and documentation tools. It is based on a presentation I did in 2009 as a talk at Google, the slides of which you can still view here.

So, without any further delay, let me present:

Safe

Debugging, Documenting and Testing Haskell Programs

Introduction

Programmers put a lot of stock in productivity. Most become intimately familiar with keyboard shortcuts in the tools they use, and will spend a great deal of time configuring their workspace for maximum productivity.

Similarly, programmers seek languages and programming paradigms that improve productivity. They want a language in which they can express their ideas clearly and succinctly, and which can be easily modified, maintained, and understood.

It has already been successfully argued elsewhere1 that functional programming languages provide substantial productivity enhancements for programmers — at least in terms of the speed at which a product is built — but what is lacking from these analyses is the less quantifiable measures that improve productivity.

Most programmers one meets are not all that enamored with Java, and yet many experienced programmers (for example, at Google) will choose Java as the language du jour for their project. I asked many such project managers why this was so, and, far and away the most common reason was “Tool Support”.

Java may not be the most fashionable language, but its development ecosystem is extremely well-rounded. Testing frameworks, refactoring editors, documentation systems, debuggers and so on are all extremely popular and well-developed in the Java world.

All too often, what is lacking from newer languages is exactly this kind of tool support. In Ruby, debugging is still done via puts, and refactoring tools are virtually nonexistent — Python suffers from a similar problem. Scala’s debugger support often falls behind Java’s. These problems plague many functional languages too: OCaml has no solid build system or library collection, and SML and Lisp have very small user-bases and are unable to provide a full set of tools.

I believe that the language Haskell, a purely functional, open-source language, is one of the best supported with regards to development ecosystem. The tools used in the Haskell world are not only fit for the Haskell community, but also for those project managers who have been choosing Java in the enterprise.

Why Haskell?

As a language, Haskell has innate productivity benefits not found in many other languages. As it is a purely functional language, many things that are considered good style are enforced by the language, reducing the amount of programmer errors that can seep into the program at runtime.

Functions cannot perform side-effects implicitly. This means that effectful functions are segregated by the type system into an impure region of the program. This is an incredible boon for writing parallelized code, as functions that perform no side-effects can always be safely executed concurrently. This separation of pure and impure makes parallelizing a function as simple as adding a small annotation.

Functions are also referentially transparent — that is, they always give the same output given the same arguments. This is a logical corollary from the lack of side-effects, as any function without side-effects can only operate on its input values in a deterministic way. This reduces the need for integration testing, as a referentially transparent function can be tested for correctness merely by a unit test, as there is no dependence on effects from other parts of the program.

There is no such thing as implicit global state in Haskell. Seeing as modifying a global state constitutes a side effect, this is not surprising. While global variables and stateful static objects in other languages have always been considered a bad code smell, it seems that a cultural distaste is not enough to prevent programmers from constantly using them. By removing this, Haskell helps to enforce good design.

Modifying a data structure is a side effect, so all data structures are immutable in Haskell. This makes Haskell’s data model far simpler than Java’s, but still equally powerful. It makes test writing incredibly easy, because one never has to check if a data structure has been correctly modified, only that the data is well formed in the output.

All of these advantages are actually restrictions. Ward Cunningham once said that what killed Smalltalk was that it was “Too easy to make a mess!”2. By restricting the programmer in this way, Haskell provides a liberating straightjacket of sorts that makes it very difficult indeed to make a mess, but still keeps programmer productivity and happiness high.

Haskell’s type system is very expressive, allowing arbitrary constraints to be placed on data and checked at compile time. Haskell enjoys a closer relationship to formal specifications in its type system than something like C. As a result, things that I call “typefails” - casts (except for use in C bindings), and the null value, both do not exist in Haskell. Despite the intricate type system, types rarely need to be explicitly declared, as Haskell’s type inference helps to keep programs short and to the point.

Haskell’s features all help to reduce runtime errors and move as much checking into the static phase as possible, so that the old programmer’s pipe dream “If it compiles, it works” is more true for Haskell than most other languages.

So, Haskell as a language provides numerous productivity benefits in terms of reducing the amount of time spent debugging or testing code. Haskell does not, however, eliminate the need for debugging or testing entirely (that is more the realm of theorem proving languages). So, we need to be able to track down bugs, document design contracts, and enforce those contracts with tests. As I mentioned earlier, Haskell’s tools are very powerful in this aspect as well.

Debugging

The most commonly used Haskell debugger is called ghci, a Haskell interpreter based around the Glasgow Haskell Compiler (GHC). It provides a REPL-style feedback loop, so that any function from inside any module can be invoked and tested by the programmer with their own input data.

Quite often, that is sufficient to debug Haskell programs, as Haskell programs are simply compositions extremely simple functions, so one merely needs to narrow down the erroneous function by examining a call tree.

ghci also provides some standard debugger equipment including breakpoints and traces, as well as type lookup - an extremely useful feature to determine the order of arguments of a function, the sort of data expected by functions, or what type has been inferred for your own functions.

But, I hate debuggers!

Sometimes people just want to insert a few printfs into their code to see what values are coming out. This style of debugging has been popular since the early days of programming, and one of the most common criticism of Haskell is that, as it is a purely functional language, side-effects like a quick printf cannot be inserted into pure code.

Somewhat surprisingly, Haskell does include a library that bends the rules precisely for this purpose, Debug.Trace. It introduces a function that takes in a value and a string, prints a string, and returns the value, all in pure code. Seeing as this is a violation of Haskell’s rules, obviously it is only intended for use in debugging. It can also cause problems in multithreaded programs - however this problem exists for printf as well.

Stub Implementations

Haskell also includes some functions that can be used to enforce runtime assertions or mark unimplemented or unwanted code branches. undefined fits into any type, and induces a crash if something tries to evaluate it. This lets you mark an area of code as unimplemented, so that the program will crash if it tries to use an unimplemented function.

Obviously unsightly crashes should be avoided, so, like asserts, they probably shouldn’t exist in production.

error is another function that works in a similar fashion to undefined, except it also takes a string to print out upon program crash. This can be used to indicate unrecoverable errors.

Testing

Most programmers these days agree that formalized, automated testing and unit testing are good things. They can be used to detect regressions, ensure your code meets the contract, and also directly reduce bugs and increase productivity.

The new Test Driven Development craze advocates never writing a line of code until you have written a failing unit test. An admirable goal, but one that I ultimately believe is impractical. So long as unit test writing seems tedious, fiddly, boring and long, your average programmers will never be disciplined enough to actually follow the Test Driven Development motto.

QuickCheck

Unit test writing is such a boring mechanical process, I have always thought that “a computer should be able to do it!”. With the Haskell library QuickCheck, a computer can do it.

I am going to write a simple reverse-list function in Haskell:


reverse :: [a] -> [a] reverse (x:xs) = reverse xs ++ [x] reverse [] = []

Normally when unit test writing, we determine the obligations of the function, and the semantics of the contract, and then devise a set of cases that test these properties.

With QuickCheck, the final step is taken out, we merely need to provide a testable specification of the contract.

A simple invariant case would be that the length of the output must be the same as the input. We can encode that as a QuickCheck property.


prop_length :: [Int] -> Bool prop_length list = length list == length (reverse list)

A perhaps more involved case would be: If I reverse a list twice, I should get the original list back:


prop_reverse2 :: [Int] -> Bool prop_reverse2 list = list == reverse (reverse list)

Sometimes our properties require qualifiers, for example: If a list is non-empty, the first element of the list should be the last element of the reversed list. QuickCheck allows us to encode such qualifiers with the ==> operator.


prop_firstlast :: [Int] -> Property prop_firstlast list = list /= [] ==> head list == last (reverse list)

Or, perhaps a more involved example:


-- Middle element remains the same for lists of odd length prop_middle list = length list `mod` 2 /= 0 ==> middle list == middle (reverse list) where halfway = length list `div` 2 middle l = head (drop halfway l)

Running the quickCheck function on any of those properties will generate hundreds of test cases (including common edge cases like small and empty lists) and assert that those properties hold true.

If a quickCheck test were to fail, QuickCheck analytically attempts to reduce the case to a simpler one that still fails - in other words, if my test were to fail for [-311515153, 3581351561, 0], this is a fairly ugly looking test case, so quickCheck often boils it down to a minimal case such as [-1,1,0] (provided that it still fails of course!).

This allows me to comprehensively test the reverse function, with hundreds of test cases, and only a few lines of code.

Ensuring Comprehensive Tests

Obviously it is possible to write a function that passes QuickCheck but is still incorrect. In our example, a function that swapped first and last elements would still pass. It would be possible to write a QuickCheck property that reads a list in reverse and ensures the list is reversed, but this is essentially re-implementing a reverse function - just as likely to be buggy as the original. Still, such a technique could be used if one was attempting to write a more optimized version of a function - quickCheck could be used to test against the slower version.

It is also (arguably more) possible to write specific unit tests that pass when the function is still incorrect, so it is unclear which one has the advantage in terms of correctness, but certainly QuickCheck has an advantage in terms of convenience. It is also possible to encode a specific test case as a QuickCheck property (simply do not take parameters).3

Suppose we introduced some random business logic into our reverse function, highly unlikely to be triggered by quickCheck.


reverse :: [a]->[a] reverse (x:xs) = if (x == 42) && (length xs == 3) then [1337] else (reverse xs) ++ [x] reverse [] = []

Obviously we should add some specific properties to test this new behavior, but it becomes clearly apparent that logic-heavy functions can easily have code branches that are missed, even by generated test cases. It would be nice to have a coverage checker to find out if any code paths are missed.

Haskell includes such a tool, Haskell Program Coverage (hpc), shipped with GHC. If you compile your program with -fhpc, and run your test cases, the command hpc report gives a summary like the following:

 97% expressions used (84/86)
  0% boolean coverage (0/1)
     100% guards (0/0)
       0% 'if' conditions (0/1), 1 always False
     100% qualifiers (0/0)
 75% alternatives used (3/4)
100% local declarations used (0/0)
100% top-level declarations used (7/7)

This shows that only 75% of possible code paths have been tested by my QuickCheck tests. The command hpc markup then allows us to view in an HTML document exactly which portions of code were not executed.

HPC demonstration for our <code
>reverse</code
> function

HPC demonstration for our reverse function

Now presented with this information, we can go and write a test for that specific case.

Documentation

QuickCheck and HUnit tests do provide a great deal of information about the contract of a function, as does the type signature. Because of the lack of side effects, often argument and return types are the only things that matter, and therefore documentation isn’t quite as important as in something like Java.

That said, obviously documentation is ideal, so Haskell provides two main documentation methodologies.

Haddock

Haddock is a documentation system quite similar to Javadoc or Perldoc. Documentation of an interface or contract is provided by specially formatted comments. Haddock can generate documentation in a variety of formats including LaTeX and HTML. HTML Haddock documentation is generated by all packages that are built on the Haskell package repository Hackage.


-- | This function is very important f :: Int -- ^ The 'Int' argument -> Float -- ^ The 'Float' argument -> IO () -- ^ The return value

Literate Haskell

Donald Knuth invented Literate Programming as a way for programmers to communicate their ideas to each other more effectively. His idea was to consider a program as a communication to other human beings rather than a set of instructions to a computer. He wrote the CWEB system that provides literate programming for C.

Typically one writes a simple LaTeX document containing some code snippets, which, when combined, form a program.

Haddock and lhs2TeX can be used to convert Literate Haskell to LaTeX documents, with excellent haskell code formatting rules. GHC can compile Literate Haskell without any issues.

Literate Programming has not been hugely successful in the enterprise world, and I believe this is a mistake. Literate programming forces the programmer to truly question their assumptions and carefully consider how the program should work. Furthermore, it makes others coming to the project more easily able to understand what is going on, as the documentation should be far more detailed than the code.

For an example of a Literate Haskell project, see my Opardum project.

You’re Going to Use Haskell

The Free Lunch is Over. As Parallel programming becomes more important, side-effected languages such as C are going to go the way of the dodo. Parallelism on merely 2 cores is difficult in traditional languages. Over thousands of cores with nested data parallelism, it’s next to impossible. Haskell provides a way for such things to become simple - for more information, please see the Data Parallel Haskell project, currently underway at UNSW.

So, Face It! You’re going to use Haskell, so make a start now!

Honourable Mention

There are lots of things I didn’t cover in this article, for example:

If I get time, I may expand this article to include them, but I guess it’s already long enough already. Thanks for bearing with me to the end!

Liam O’Connor-Davis

3 April 2010


  1. Ericsson encountered massive productivity increases using Erlang. Also See Why Functional Programming Matters

  2. See What Killed Smalltalk could Kill Ruby, an excellent talk by David Martin.

  3. It should be noted that Haskell does have a more traditional unit testing framework, HUnit, good for general testing scenarios such as monadic code where QuickCheck is not appropriate.