Почему этот код делится на ноль?

У меня есть небольшая программа Haskell, и мне любопытно, почему при ее запуске генерируется исключение с нулевым исключением (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]

Может ли кто-нибудь помочь мне понять, что здесь происходит?

Ответы

Ответ 1

Мы можем уменьшить это до

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

Обратите внимание, что это работает, если вы используете Word16, Int, Integer или любое количество типов, но сбой при использовании Word8, поскольку B.unpack дает нам! Так почему это терпит неудачу? Ответ находится в исходном коде на Codec.Utils.toTwosComp. Вы можете видеть, что он вызывает toBase 256 (abs x), где x - это аргумент.

Тип toBase - не экспортируется из модуля Codec.Utils и без явной сигнатуры типа в источнике, но вы можете увидеть это, поместив определение в файл и спросив GHCi, что такое тип (:t toBase),

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

Таким образом, аннотирование типов явно, toTwosComp вызывает toBase (256 :: Word8) (abs x :: Word8). Что 256 :: Word8?

GHCi> 256 :: Word8
0

Oops! 256 > 255, поэтому мы не можем удерживать его в Word8, и он переполняется беззвучно. toBase, в процессе его базового преобразования, делит на используемую базу, поэтому он заканчивается делением на ноль, создавая поведение, которое вы получаете.

Какое решение? Преобразуйте Word8s в Ints с помощью fromIntegral, прежде чем передавать их в toTwosComp:

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

Лично это поведение меня немного беспокоит, и я думаю, что toTwosComp должно, вероятно, сделать такое преобразование, вероятно, Integer, чтобы оно работало с целыми типами каждого размера; но это повлечет за собой штраф за производительность, который разработчикам может не понравиться. Тем не менее, это довольно запутанный провал, который требует понимания источника. К счастью, это очень легко обойти.

Ответ 2

map (head . toTwosComp) [1, 2, 3, 4]

работает нормально, а

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

вызывает описанное вами исключение. Посмотрим, какая разница.

> :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 может вызвать проблему. Посмотрим

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

По-видимому, нам нужно преобразовать Word8 в другой целочисленный тип.

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

Это работает!