Skip to content
July 5, 2007 / cdsmith

Find the Bug

Here’s a pretty tricky bug I hunted down yesterday, with the help of oerjan on #haskell.  If you weren’t there to see, it’s a lot of fun.  Here’s the code:

foo n = product [24 - n .. 23] `div` product [1 .. n]
bar n = length (takeWhile ((< 1000000) . foo) [1 .. n])
baz n = if bar n == n then n else bar n

And here’s the result:

cdsmith@devtools:~/haskell/projects$ ghci Strange.hs
GHCi, version 6.7.20070703: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( Strange.hs, interpreted )
Ok, modules loaded: Main.
*Main> bar 10
9
*Main> baz 10
10

The question: Why does bar 10 evaluate to something different from baz 10?  By looking at the definition of baz, you might convince yourself that baz and bar do the same thing.  Apparently not.

Scroll down for the answer.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The problem is numeric overflow.  The standard library length function has a result type of Int.  The intermediate result in foo is too large to fit in the data type Int.  The bar function has type forall a. Integral a => a -> Int.  When calling bar directly, this isn’t a problem because the argument defaults to type Integer and foo is evaluated entirely in arbitrary precision numbers.  However, when baz is evaluated, there is a comparison bar n == n, so the result of bar must have the same type as its argument.  The result is known to be Int because the return type of bar is not polymorphic.  So the argument n must also be of type Int.  Then foo's inferred type is Int -> Int, which causes the overflow.

All clear now?

If one wanted to take a lesson from this, it would be one of the following:

  • Be careful with types.
  • General purpose functions like length shouldn’t use dangerous types like Int.
  • Int should be made less dangerous by throwing an exception on overflow.

Let’s take a poll: which lesson do you draw from it?  Post in the comments.

About these ads

7 Comments

Leave a Comment
  1. sigfpe / Jul 5 2007 3:57 pm

    When ghci responds with simply ’10’ it hasn’t really given you a full answer as it hasn’t told you the type of the result. Seems to me it ought to also print out the type.

  2. Brent / Jul 5 2007 4:49 pm

    I’m always wary of letting the compiler infer numeric types for me, since (as you found) it can screw around with the semantics. You may not be the sort to put type annotations on every top-level declaration, but I think this code is a great argument for doing so — at the very least it forces you to think about what you want, and at best the type checker will often catch this sort of thing for you. Adding type annotations to the above code, one quickly runs into type errors — unless you make everything Int -> Int, which should (hopefully) give pause when doing something with (e.g.) large products.

  3. Koray / Jul 5 2007 8:06 pm

    Also see Data.List.genericLength.

  4. augustss / Jul 6 2007 2:47 am

    The mistake was made in the Haskell 1.0 to 1.1 changes. The length function used to have the type genericLength does.

  5. Anonymous / Jul 6 2007 5:21 am

    I strongly claim that there is no need to change the ‘length’ function. To get a ‘length’ result that would overflow Int you would need to fully traverse a list (i.e. the Haskell singly linked list) with more than 2 billion elements.

    Essentially no one should be applying ‘length’ to such a long list, and there is ‘genericLength’ that covers this (and all other Integral) cases for the few people who do it.

    The problem here is that ‘foo’ computes something up to 23! which is much bigger than Int’s 2^31. And he did not specify ‘Integer’ and he did not change the ‘default (Int,Integer)’ behavior (note that he is also using the interactive GHCI system). If I want to use numbers and not worry about defaulting then I put ‘default ()’ in the file.

    As for an overflow checking Int, you could make a newtype that checked overflow. You could even make a type

    data CheckedInt = I Int | Overflow | Underflow | DivideByZero | ….

    and use it instead.

  6. cdsmith / Jul 6 2007 8:01 am

    Hmm. If there’s one thing this example makes clear, it’s that the implications of defining length with type ([a] -> Int) go far beyond the range of values that will ever be returned by length. They also have to do with what you might want to do with the result of length, and the types of the other values in those computations. The simple fact that Int will never overflow its specified range is not enough to establish that Int is the correct type.

    I’m sure there were reasons for the choice not to do length = genericLength; but they will have to do with performance. Absent the desire for performance optimizations, I have a hard time seeing why Int would be considered the “right” choice for length, as compared to (Integral a) => a. Ah well, everyone makes trade-offs.

    No one’s yet taken up my favorite answer, which is that there’s something wrong with the Int type. The combination of (a) not defining the size of Int, and (b) permitting wrap-on-overflow basically means that the behavior of Int is undefined in certain cases. Unchecked overflow for types like Int32 and Word32 would be quite justifiable; but IMO unchecked overflow for Int is just plain wrong.

  7. Pozorvlak / Jul 9 2007 9:30 am

    Well, here are the lessons I took from it:Like, whoa! Type inference can actually cause bugs? Dude. I thought it was meant to prevent them…
    Maybe one should start by using an algorithm that’s less likely to overflow :-)
    I’m generally not in favour of arithmetic throwing exceptions, but that’s largely down to my time spent doing scientific numeric stuff, where exceptions were a major pain. And Int has no equivalent of Infinity, so maybe that’s the least bad option.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 74 other followers

%d bloggers like this: