Chapter 16
IN THIS CHAPTER
Understanding Haskell bugs
Locating and describing Haskell errors
Squashing Haskell bugs
Most application code contains errors. It’s a blanket statement that you may doubt, but the wealth of errors is obvious when you consider the number of security breaches and hacks that appear in the trade press, not to mention the odd results that sometimes occur from seemingly correct data analysis. If the code has no bugs, updates will occur less often. This chapter discusses errors from a pure functional language perspective; Chapter 17 looks at the same issue from an impure language perspective, which can differ because impure languages often rely on procedures.
After you identify an error, you can describe the error in detail and use that description to locate the error in the application code. At least, this process is the theory that most people go by when finding errors. Reality is different. Errors commonly hide in plain view because the developer isn’t squinting just the right way in order to see them. Bias, perspective, and lack of understanding all play a role in hiding errors from view. This chapter also describes how to locate and describe errors so that they become easier to deal with.
Knowing the source, location, and complete description of an error doesn’t fix the error. People want applications that provide a desired result based on specific inputs. If your application doesn’t provide this sort of service, people will stop using it. To keep people from discarding your application, you need to correct the error or handle the situation that creates the environment in which the error occurs. The final section of this chapter describes how to squash errors —for most of the time, at least.
A bug occurs when an application either fails to run or produces an output other than the one expected. An infinite loop is an example of the first bug type, and obtaining a result of 5 when adding 1 and 1 is an example of the second bug type. Some people may try to convince you that other kinds of bugs exist, but these other bugs end up being subsets of the two just mentioned.
However, functional languages tend to bring their own assortment of bugs into applications, and knowing what these bugs are is a good idea. They’re not necessarily new bugs, but they occur differently with functional languages. The following sections consider the specifics of bugs that occur with functional languages, using Haskell as an example. These sections provide an overview of the kinds of Haskell-specific bugs that you need to think about, but you can likely find others.
Functional languages generally avoid mutable variables by using recursion. This difference in focus means that you’re less apt to see logic errors that occur when loops don’t execute the number of times expected or fail to stop because the condition that you expected doesn’t occur. However, it also means that stack-related errors from infinite recursion happen more often.
Haskell is a lazy language for the most part, which means that it doesn’t perform actions until it actually needs to perform them. For example, it won’t evaluate an expression until it needs to use the output from that expression. The advantages of using a lazy language include (but aren’t limited to) the following:
withFile "MyData.txt" ReadMode handle >>= putStr
If you looked at the code from a procedural perspective, you would think that it should work. The problem is that lazy evaluation using withFile
means that Haskell closes handle
before it reads the data from MyData.txt
. The solution to the problem is to perform the task as part of a do
, like this:
main = withFile "MyData.txt" ReadMode $ handle -> do
myData <- hGetLine handle
putStrLn myData
However, by the time you create the code like this, it really isn't much different from the example found in the “Reading data” section of Chapter 13. The main advantage is that Haskell automatically closes the file handle for you. Offsetting this advantage is that the example in Chapter 13 is easier to read. Consequently, lazy evaluation can impose certain unexpected restrictions.
Haskell generally provides safe means of performing tasks, as mentioned in several previous chapters. Not only is type safety ensured, but Haskell also checks for issues such as the correct number of inputs and even the correct usage of outputs. However, you may encounter extremely rare circumstances in which you need to perform tasks in an unsafe manner in Haskell, which means using unsafe functions of the sort described at https://wiki.haskell.org/Unsafe_functions
. Most of these functions are fully described as part of the System.IO.Unsafe
package at http://hackage.haskell.org/package/base-4.11.1.0/docs/System-IO-Unsafe.html
. The problem is that these functions are, as described, unsafe and therefore the source of bugs in many cases.
The same discussion explores other uses for unsafePerformIO
. For example, one of the code samples shows how to create global mutable variables in Haskell, which would seem counterproductive, given the reason you're using Haskell in the first place. Avoiding unsafe functions in the first place is a better idea because you open yourself to hours of debugging, unassisted by Haskell’s built-in functionality (after all, you marked the call as unsafe).
As with most language implementations, you can experience implementation-specific issues with Haskell. This book uses the Glasgow Haskell Compiler (GHC) version 8.2.2, which comes with its own set of incompatibilities as described at http://downloads.haskell.org/~ghc/8.2.2/docs/html/users_guide/bugs.html
. Many of these issues will introduce subtle bugs into your code, so you need to be aware of them. When you run your code on other systems using other implementations, you may find that you need to rework the code to bring it into compliance with that implementation, which may not necessarily match the Haskell standard.
It’s essential to understand that the functional nature of Haskell and its use of expressions modifies how people commonly think about errors. For example, if you type x = 5/0 and press Enter in Python, you see a ZeroDivisionError
as output. In fact, you expect to see this sort of error in any procedural language. On the other hand, if you type x = 5/0 in Haskell and press Enter, nothing seems to happen. However, x
now has the value of Infinity
. The fact that some pieces of code that define an error in a procedural language but may not define an error in a functional language means that you need to be aware of the consequences.
To see the consequences in this case, type :t x and press Enter. You find that the type of x
is Fractional
, not Float
or Double
as you might suppose. Actually, you can convert x
to either Float or Double by typing y = x::Double or y = x::Float and pressing Enter.
The Fractional type is a superset of both Double
and Float
, which can lead to some interesting errors that you don't find in other languages. Consider the following code:
x = 5/2
:t x
y = (5/2)::Float
:t y
z = (5/2)::Double
:t z
x * y
:t (x * y)
x * z
:t (x * z)
y * z
The code assigns the same values to three variables, x
, y
, and z
, but of different types: Fractional
, Float
, and Double
. You verify this information using the :t
command. The first two multiplications work as expected and produce the type of the subtype, rather than the host, Fractional
. However, notice that trying to multiply a Float
by a Double
, something you could easily do in most procedural languages, doesn't work in Haskell, as shown in Figure 16-1. You can read about the reason for the lack of automatic type conversion in Haskell at https://wiki.haskell.org/Generic_number_type
. To make this last multiplication work, you need to convert one of the two variables to Fractional first using code like this: realToFrac(y) * z
.
x = 5/2
x = x + 1
x
In Python, you see an output of 3.5, which is what anyone working with procedural code will expect. However, this same code causes Haskell to enter into an infinite loop because the information is evaluated as an expression, not as a procedure. The output, when working with compiled code, is <<loop>>
, which you can read about in more detail at https://stackoverflow.com/questions/21505192/haskell-program-outputs-loop
. When using WinGHCi (or another interpreter), the call will simply never return. You need to click the Pause button (which looks like the Pause button on a remote) instead. A message of Interrupted
appears to tell you that the code, which will never finish its work, has been interrupted. The fact that Haskell actually detects many simpler infinite loops and tells you about them says a lot about its design.
Even though this section isn’t a complete list of all the potential kinds of errors that you see in Haskell, understand that functional languages have many similarities in the potential sources of errors but that the actual kinds of errors can differ.
Haskell, as you’ve seen in the error messages in this book, is good about providing you with trace information when it does encounter an error. Errors can occur in a number of ways, as described in Chapter 17. Of course, the previous sections have filled you in on Haskell exceptions to the general rules. The following sections give an overview of some of the ways to fix Haskell errors quickly.
Haskell provides the usual number of debugging tricks, and the IDE you use may provide others. Because of how Haskell works, your first line of defense against bugs is in the form of the messages, such as error and CallStack output, that Haskell provides. Figure 16-1 shows an example of an error output, and Figure 16-2 shows an example of CallStack output. Comparing the two, you can see that they’re quite similar. The point is that you can use this output to trace the origin of a bug in your code.
During the debugging process, you can use the trace
function to validate your assumptions. To use trace
, you must import Debug.Trace
. Figure 16-3 shows a quick example of this function at work.
Haskell does provide other debugging functionality. For example, you gain full access to breakpoints. As with other languages, you have methods available for determining the status of variables when your code reaches a breakpoint (assuming that the breakpoint actually occurs with lazy execution). The article at https://wiki.haskell.org/Debugging
offers additional details.
For most programming languages, you can use the terms error and exception almost interchangeably because they both occur for about the same reasons. Some languages purport to provide a different perspective on the two but then fail to support the differences completely. However, Haskell actually does differentiate between the two:
error
assert
Control.Exception.catch
Debug.Trace.trace
Prelude.catch
Control.Exception.catch
Control.Exception.try
IOError
Control.Monad.Error
3.149.228.138