Catching Before Production: Assert Statements in Haskell

We've spent a lot of time this month going over exceptions, which are ways to signal within our program that something unexpected has happened. These will often result in an early termination for our program even if we catch them. But by catching them, we can typically provide more helpful error messages and logs. Exceptions are intended for use in production code. You don't want them to ever go off, but they are there .

However, there are other bugs that you really want to catch before they ever make it into production. You don't want to formally recognize them in the type system because other parts of the program shouldn't have to deal with those possibilities. In these cases, it is common practice for programmers to use "assert" statements instead.

We'll start with a simple example in Python. We'll write a function to adjust a price, first by subtracting and second by taking the square root. Of course, you cannot take the square root of a negative number (and prices shouldn't be negative anyways). So we'll assert that the price is non-negative before we take the root.

def adjustPrice(input):
  adjustedDown = input - 400.0
  assert (adjustedDown >= 0)
  return $ sqrt(adjustedDown)

In Haskell we also have an assert function. It looks a bit like throw in the sense that its type signature looks pure, but can actually cause an error.

assert :: Bool -> a -> a

If the boolean input is "true", nothing happens. The function will return the second input as its output. But if the boolean is false, then it will throw an exception. This is useful because it will provide us with more information about where the error occurred. So let's rewrite the above function in Haskell.

adjustPrice :: Double -> Double
adjustPrice input = assert (adjustedDown >= 0.0) (sqrt adjustedDown)
  where
    adjustedDown = input - 400.0

If we give it a bad input, we'll get a helpful error message with the file and line number where the assertion occurred:

main :: IO ()
main = do
  let result = adjustPrice 325.0
  print result

...

>> stack exec my-program

my-program: Assertion failed
CallStack (from HasCallStack):
  assert, called at src/Lib.hs in MyProgram-0.1.0.0:Lib

Without using the asssert, our function would simply return NaN and continue on! It would be much harder for us to track down where the bug came from. Ideally, we would catch a case like this in unit testing. And it might indicate that our "adjustment" is too high (perhaps it should be 40.0 instead of 400.0).

For the sake of efficiency, assert statements are turned off in executable code. This is why it is imperative that you write a unit test to uncover the assertion problem. In order to run your program with assertions, you'll need to use the fno-ignore-asserts GHC option. This is usually off for executables, but on for test suites.

We have one more concept to talk about with exception handling, so get ready for that! If you want a summary of all the topics we talked about this month, make sure to subscribe to our monthly newsletter!

Previous
Previous

A Brief Look at Asynchronous Exceptions

Next
Next

Resources and "Bracket"