16

I have a small Haskell program, and am curious why a divide by zero exception gets thrown when I run it (GHC 7.0.3)

import qualified Data.ByteString.Lazy as B
import Codec.Utils

convert :: B.ByteString -> [Octet]
convert bs = map (head . toTwosComp) $ B.unpack bs

main = putStrLn $ show $ convert $ B.pack [1, 2, 3, 4]

Can anyone help me understand what's going on here?

Aziz Shaikh
  • 16,245
  • 11
  • 62
  • 79
Litherum
  • 22,564
  • 3
  • 23
  • 27

2 Answers2

17

We can reduce this to

GHCi> toTwosComp (1 :: Word8)
*** Exception: divide by zero

Note that this works if you use Word16, Int, Integer, or any number of types, but fails when using Word8, as B.unpack gives us! So why does it fail? The answer is found in the source code to Codec.Utils.toTwosComp. You can see that it calls toBase 256 (abs x), where x is the argument.

The type of toBase — not exported from the Codec.Utils module, and without an explicit type signature in the source, but you can see this by putting the definition in a file and asking GHCi what the type is (:t toBase), is

toBase :: (Integral a, Num b) => a -> a -> [b]

So, annotating types explicitly, toTwosComp is calling toBase (256 :: Word8) (abs x :: Word8). What's 256 :: Word8?

GHCi> 256 :: Word8
0

Oops! 256 > 255, so we can't hold it in a Word8, and it overflows silently. toBase, in the process of its base conversion, divides by the base being used, so it ends up dividing by zero, producing the behaviour you're getting.

What's the solution? Convert the Word8s to Ints with fromIntegral before passing them to toTwosComp:

convert :: B.ByteString -> [Octet]
convert = map convert' . B.unpack
  where convert' b = head $ toTwosComp (fromIntegral b :: Int)

Personally, this behaviour worries me a bit, and I think toTwosComp should probably do such a conversion itself, likely to Integer, so that it works with integral types of every size; but this would incur a performance penalty that the developers might not like the idea of. Still, this is a pretty confusing failure that requires source-diving to understand. Thankfully, it's very easy to work around.

ehird
  • 40,602
  • 3
  • 180
  • 182
5
map (head . toTwosComp) [1, 2, 3, 4]

works fine while

map (head . toTwosComp) $ B.unpack $ B.pack [1, 2, 3, 4]

causes the exception you've described. Let's see what's the difference.

> :t [1, 2, 3, 4]
[1, 2, 3, 4] :: Num t => [t]
> :t unpack $ pack $ [1, 2, 3, 4]
unpack $ pack $ [1,2,3,4] :: [Word8]

Word8 might be causing the problem. Let's see

> toTwosComp (1 :: Word8)
*** Exception: divide by zero

So apparently we have to convert Word8 to other integer type.

> map (head . toTwosComp . fromIntegral) $ B.unpack $ B.pack [1, 2, 3, 4]
[1,2,3,4]

It works!

Jan
  • 11,636
  • 38
  • 47