Like @kosmikus said, both Int8
and Int16
are implemented using an Int#
, which is 32 bit-wide on 32-bit architectures (and Word8
and Word16
are Word#
under the hood). This comment in GHC.Prim explains this in more detail.
So let's find out why this implementation choice results in the behaviour you see:
> let x = unsafeCoerce (-1 :: Int8) :: Word8
> show x
"-1"
The Show
instance for Word8
is defined as
instance Show Word8 where
showsPrec p x = showsPrec p (fromIntegral x :: Int)
and fromIntegral
is just fromInteger . toInteger
. The definition of toInteger
for Word8
is
toInteger (W8# x#) = smallInteger (word2Int# x#)
where smallInteger
(defined in integer-gmp) is
smallInteger :: Int# -> Integer
smallInteger i = S# i
and word2Int#
is a primop with type Word# -> Int#
- an analog of reinterpret_cast<int>
in C++. So that explains why you see -1
in the first example: the value is just reinterpreted as a signed integer and printed out.
Now, why would adding 0
to x
give you 255
? Looking at the Num
instance for Word8
we see this:
(W8# x#) + (W8# y#) = W8# (narrow8Word# (x# `plusWord#` y#))
So it looks like the narrow8Word#
primop is the culprit. Let's check:
> import GHC.Word
> import GHC.Prim
> case x of (W8# w) -> (W8# (narrow8Word# w))
255
Indeed it is. That explains why adding 0 is not a no-op - Word8
addition actually clamps down the value to the intended range.