Integer Types
C, C++, and Rust have a wide range of integer types that represent 8-bit,
16-bit, 32-bit, 64-bit, and even 128-bit integers, both in signed and unsigned
versions. What they lack is an integer type that supports arbitrarily large
numbers, numbers that may take 200 or even thousands of bits to represent.
Haskell offers less fine-grained control over the number of bits used to
represent fixed-width integers.1 There's the Int
type, which is whatever
Haskell considers the system integer type, usually 64 bits on modern systems.
This is the most commonly used integer type in Haskell. The other integer type
Haskell offers is Integer
. This is an integer type that can represent
arbitrarily large numbers.
The different choice of integer types offered by C, C++, Rust on the one hand
and Haskell on the other hand is a reflection of the types of problems these
languages are designed to solve. C, C++, and Rust are systems programming
languages, which need to provide precise control over the machine-level
representations of data. They also do not offer any data types built into the
language that cannot be represented directly at the machine level and therefore
incur overhead to manipulate. The Integer
type is such a type because
integers of arbitrary size can only be represented as arrays of machine
integers (words) and arithmetic operations need to be implemented manually by
appropriately combining machine-level instructions to pairs of words in these
arrays of words. Haskell is focused on supporting powerful abstractions for
programming high-level applications elegantly and with little effort. This
makes it less necessary to precisely control the number of bits used to
represent a fixed-width integer, and using the Integer
type whenever you have
no idea how large the numbers in your program can get relieves you of any
worries that your code may misbehave due to number overflow.
The operations you can perform on integers are the ones you'd expect:
Addition
>>> 5 + 3
8
Subtraction
>>> 5 - 3
2
Multiplication
>>> 5 * 3
15
Exponentiation
>>> 5 ^ 3
125
Division and Remainder
The division operation is called div
(divide) instead of (/)
2 because
(/)
is floating-point division, discussed shortly. Since div
's name is made
up of letters, you need to enclose it in backticks to use it in infix notation:
>>> 5 `div` 3
1
If we can perform integer division, we should also be able to take the
remainder of dividing one number by another. This operator is called mod
(modulo):
>>> 5 `mod` 3
2
There are two other operators for integer division and remainder: quot
(quotient) and rem
(remainder). They differ from div
and mod
in how they
round their results when operating on negative numbers. There are three
problems with these two operators:
- Their exact behaviour is not part of the Haskell specification.
- The description of their difference to
div
andmod
on Hackage makes no sense to me. - Even observing their behaviour and coming up with a sensible and simple description of their precise behaviour seemed difficult.
Consequently, I almost never use quot
and rem
. Who likes to call functions
whose behaviour they don't understand and which may correspondingly make their
code compute the wrong thing?
It is common enough to want to perform integer division and compute the
remainder of the division at the same time to have a single operator for both.
This operator comes in two versions again: divMod
and quotRem
. It returns a
pair as a result. The first component is the same result that div
(or quot
)
would have produced. The second component is the result that mod
(or rem
)
would have produced:
>>> 5 `divMod` 3
(1, 2)
Comparison Operators
In addition to maniplating them using basic arithmetic operators, numbers can be compared. You can test whether two numbers are equal:
>>> 3 == 5
False
>>> 3 /= 5
True
It should be obvious how these operators work. The only thing to be
highlighted is that the "not equal" operator is (/=)
not (!=)
as in most
other programming languages. I guess this is a result of Haskell's
mathematical pedigree because (/=)
is an ASCII approximation of \(\ne\).
You also have your standard inequality operators:
>>> 3 < 5
True
>>> 3 > 5
False
>>> 3 <= 5
True
>>> 3 >= 5
False
At this point, it is sufficient to know that all these operations are supported
by integers. Later, we will see that these operators are part of type
classes, a concept similar to Java interfaces or traits in Rust. Addition,
subtraction, and multiplication are operations supported by any number type,
represented by the type class Num
. Integer division is supported by any
integral number type, represented by the type class Integral
. The
comparison operators are split into two type classes, Eq
and Ord
, which
represent types that can be tested for equality and types that have an ordering
defined on them, respectively. Some types, such as binary trees, may be
comparable for equality but there is generally no meaningful notion of one tree
being less than another tree. So the binary tree would be an instance of the
Eq
type class but not of the Ord
type class. Functions cannot even be
effectively tested for equality, so there isn't even an equality operator for
functions. For now, we will ignore type classes and simply be happy that the
built-in types support exactly the operations we expect.
-
There are additional modules you can load to use number types of a particular width, such as
Word8
,Word16
, etc. You normally don't need them because you will care about the width of the integer-type you use only in system-level or performance-oriented programming, something you will rarely do in Haskell. ↩ -
Whenever I mention an operator on its own, without arguments to which it is applied, I will enclose it in parentheses because this is how you would write these operators in prefix notation (see here) and also in type signatures. ↩