SC1

Statistical Computing 1

Debugging


It is almost impossible to write non-trivial code without bugs, where a “bug” is any flaw that leads to a computation producing an unintended result or causing an error to be thrown. There are many types of bugs: from simple spelling mistakes to logical flaws. In some cases, the source of a bug is obvious and in others a bug arises from misunderstanding something very subtle and it can be difficult to identify the root cause of the problem.

General tips

There are several tips online, that are language-agnostic for the most part. For example, in Advanced R.

Many people view debugging as essentially a specific application of the scientific method. One should try to understand the phenomena by performing experiments and testing hypotheses, often from the perspective of what “should happen” if everything was working correctly.

Creating a minimal working example is also the point at which you could submit a bug report, if the bug is not in your own code but in someone else’s, e.g. a package developer. Developers often cannot fix a bug if you provide a long example of unexpected behaviour in your own code; they want to see a MWE of unexpected behaviour for functions in their own package.

Finally, many people have strong opinions on debugging. There are many approaches that work, and are used by various people. It is not uncommon to spend many hours debugging a specific issue on occasion. However, if you routinely find that it takes many hours to fix bugs, you may want to reconsider your approach.

The advice in Advanced R is also helpful:

You shouldn’t need to use these tools when writing new functions. If you find yourself using them frequently with new code, reconsider your approach. Instead of trying to write one big function all at once, work interactively on small pieces. If you start small, you can quickly identify why something doesn’t work, and don’t need sophisticated debugging tools.

An example of debugging in R

Here we start with an implementation of a function that takes arguments x and array, and returns the first (i.e. smallest) index of the array i such that array[i] = x. This is a problematic implementation for a few reasons, but let’s go with it. We can’t all write perfect code all the time.

find.first <- function(x, array) {
  i <- 1
  while (TRUE) {
    if (array[i] == x) return(i)
    i <- i + 1
  }
}

Perhaps an existing test checked whether the function works on a simple example.

3 == find.first(2, c(1,3,2,4,5))
## [1] TRUE

However, now the code is being used in some other function foo that finds the index of the first occurrence of 1 in an array. This function foo is not called directly, but instead is called by some other function bar.

foo <- function(array) {
  return(find.first(1, array))
}

bar <- function() {
  x <- sample(100, 99)
  foo(x)
}
for (j in 1:100) {
  bar()  
}
## Error in if (array[i] == x) return(i): missing value where TRUE/FALSE needed

It is clear that there is a bug in the code, but the error message is not very informative unless you know exactly how bar works. It is not even obvious which function has thrown the error. To get more information it is useful to use R’s traceback() function, which is also sometimes accessible graphically in RStudio.

If you call traceback(), you should see information such as:

3. find.first(1, array)
2. foo(x)
1. bar()

possibly also with some line numbers. This gives you the call stack that led to the error. At least now we know that the error was thrown by find.first.

We may not realize yet why the error was thrown, except that it seems that array[i] == x did not evaluate to TRUE or FALSE for some i. To try to determine more, we can first try to find a specific random seed that leads to the error.

It is not difficult to find a specific situation in which the error occurs.

set.seed(15)
for (j in 1:100) {
  print(j)
  bar()  
}
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7
## [1] 8
## [1] 9
## [1] 10
## [1] 11
## [1] 12
## [1] 13
## [1] 14
## [1] 15
## [1] 16
## [1] 17
## [1] 18
## [1] 19
## [1] 20
## [1] 21
## [1] 22
## [1] 23
## [1] 24
## [1] 25
## [1] 26
## [1] 27
## [1] 28
## [1] 29
## [1] 30
## [1] 31
## [1] 32
## [1] 33
## [1] 34
## [1] 35
## [1] 36
## [1] 37
## Error in if (array[i] == x) return(i): missing value where TRUE/FALSE needed

So we have an error on one of the iterations of the for loop, with a seed of 15. Annoyingly, the exact iteration does seem to depend on the version of R that is used. To get more detail, we can start R’s debugger on that iteration.

set.seed(15)
for (j in 1:100) {
  if (j == <whatever the index is>) browser()
  bar()  
}

The browser() call opens an interactive debugging environment where you can type

Typically, you will use a combination of ‘n’, ‘s’ and typing variable names to inspect their values, in order to locate a problem.

For example, with the code above, you can use these commands to eventually find that the array in the call to find.first does not include the value 1, which ultimately causes the while loop to query array at an index larger than its length.

This is certainly a bug, but the specific mistake made depends on what the purpose of the find.first function really is. If it is expected that the input x is a member of the array array then it would make sense to throw a more informative error when x is not in array, e.g. by checking before the while loop, or by throwing the error when i exceeds length(array). Alternatively, it may be the case that find.first should return NA or NULL or 0 when x is not in the array.

Once the bug is fixed, one can add appropriate tests to ensure that the same mistake is not made again, e.g. if one refactors the code.