Last week we explored the many different string types in Haskell. But this isn't the only situation where we seem to have an abundance of similar types. We can also see this in Haskell's numeric types. Again, we have the issue that we often want to represent numbers in slightly different ways. But Haskell's type system forces us to have different types for each different case. Interoperating between these types can be very painful.
The most familiar integral type is the simple
Int type. This represents your standard machine-varying, bounded, signed integer. According to the documentation, it is guaranteed to have a range of at least -2^29 to 2^29. So if you’re on a 32-bit machine, your
Int might have a different set of bounds than on a 64-bit machine. Luckily,
Int is a member of the
Bounded typeclass. So in your code, you can always query the max and min bounds to check for overflow.
class Bounded a where minBound :: a maxBound :: a
Now, suppose you don’t want to be at the whims of the machine for how large a value you can store. Sometimes, you want to know exactly how much memory your int will take. There are several different
Int types that allow you to do exactly this. We have
Int64. They allow you to have more definite bounds on your number, while giving you the same basic functionality as
Int8 is 8 bits,
Int16 is 16 bits, and so on.
Now, there are also circumstances where you want your integer to be unbounded. In this case, you’ll need to use the
Integer type. This type establishes no limit on how big your number can be. It does not implement the
Bounded typeclass. Naturally, this comes at a performance penalty. If your number is too large to fit in a single register in memory, Haskell will use a byte array to represent the number. Operations on this number will be slower. Hardware is designed to make mathematical operations on smaller values extremely fast. You won’t be able to get these speedups on larger numbers. But if you need the higher bounds then you don't have a choice.
Once you understand the different
Int classes, next up are the
Word types. Each
Int size has a corresponding
Word type. These types work the same way, except they are unsigned. So whereas an
Int8 goes from -128 to 127, a
Word8 can contain values from 0 to 255. In fact,
Data.ByteString has a function to give you a list of
Word8 values as a representation of the byte string.
So what lessons then can we take from these different integral types? How do we choose what type to use? For most cases,
Int is probably fine. Depending on the domain, you may be able to limit the number of bytes that you need to use. This is where the various sizes of integers come into play. If you know your value will be positive and not negative, definitely use
Word types instead of
Int types. This will give you more wiggle room against those bounds. If you're dealing with values that might exceed 64-bit bounds, you have no choice but to use
Integer. Be aware that if the values are this large, you'll face a performance penalty.
The Integral Typeclass
You'll often come across a situation where you want to write a more general function on numbers. You don't know exactly what type you'll be dealing with. But you know it'll be one of these integer types. Like a good Haskell developer, you want your code to be as polymorphic as possible. This is what the
Integral typeclass is for. It encapsulates a few different pieces of functionality.
First, it facilitates the conversion between the different integral types. It supplies a
toInteger function with the type:
toInteger :: Integral a => a -> Integer
This allows you to use the unbounded
Integer type as a go-between amongst your integral types. It allows you to write reasonably polymorphic code. There can also be drawbacks though. By using the lowest common denominator, you might make your program's performance worse. So for performance critical code, you might need to reason more methodically about which type you should use.
The other functions within the
Integral typeclass deal with division on integral types. For instance, there are quotient and remainder functions. These allow you to perform integral division when you don't know the exact type.
class Integral a where toInteger :: a -> Integer quot :: a -> a -> a -- ^^ Integer division, e.g. 9 `quot` 2 = 4 rem :: a -> a -> a -- ^^ Remainder, e.g. 9 `rem` 2 = 1 div :: a -> a -> a mod :: a -> a -> a
As a note, the
mod functions are like
5 / 2. But that’s the price of a strong typed system.
Floating Point Numbers
Of course, we also have a couple different types to represent floating point numbers. Haskell has two main floating point types:
Float type is a single-precision floating point number. And of course,
Double is double precision. They represent the same basic concept, but
Double allows a larger range of values with more precision. At the same time, it takes up twice as much memory as a
Float. Converting between these two types is easy using the following functions:
float2Double :: Float -> Double double2Float :: Double -> Float
There is a typeclass that encapsulates these types (as well as a couple more obscure versions). This is the
Floating typeclass. It allows a host of different operations to be performed on these types. A lot of these are mathematical functions.
For instance, one of these functions is just
pi. So if your function takes any type in the
Floating typeclass, you can still get a reliable value for pi.
Floating also incorporates other math concepts as well, like square roots, exponents, trigonometric functions, and so on.
Other Numeric Typeclasses
There are a few other numeric typeclasses. They encapsulate behavior both for floating numbers and integers. For instance, we have the
Real typeclass which allows us to convert anything to a
Rational number. There is also the
Fractional typeclass. It allows us to perform certain operations that are natural on fractions. These include calculating reciprocals and performing true (non-integer) division. We can then mash these two classes together to get
RealFrac. This typeclass allows us to express a number as a true fraction (so a tuple of two integral numbers). It has several other useful functions like
truncate, and so on.
We’ve gone over some of the conversions between similar types. But it’s difficult to keep track of all the different ways to convert between values. For a more exhaustive list, check out Gentle Introduction to Haskell. Besides what we've covered, two types of transitions stand out the most.
First, there is
fromIntegral. This allows you to convert from any integral type to any numeric type. It should be your go-to when you need a floating point number but have some integer type. Second, there is the process of going from a floating point number to an integer type. You’ll generally use one of
ceiling, depending on your desired coercion. Finally, remember the
toInteger function. It is generally the answer when going between integral types.
fromIntegral :: (Integral a, Num b) => a -> b round :: (Fractional a, Integral b) => a -> b floor :: (Fractional a, Integral b) => a -> b ceiling :: (Fractional a, Integral b) => a -> b toInteger :: (Integral a) => a -> Integer
If you do any level of web programming, you’ll likely be encoding things in JSON using the
Data.Aeson library. We’ll go more in depth on this library in a later article (it’s super useful). But it uses the
Scientific type, which represents numbers in “scientific” notation. In this format, you have a base multiplied by 10 to a certain power. You won’t encounter this type too often, but it’s useful to know how to construct it. In particular you’ll often want to take simple integer or floating point values and convert them back and forth. Here are some code examples how:
-- Creates 70000 createScientificNum :: Scientific createScientificNum = scientific 7 4 -- Conversion to float (i.e. 70000.0) convertToFloat :: Double convertToFloat = toBoundedRealFloat createScientificNum -- Convert to integer, might fail if the scientific is not an integer convertToInt :: Maybe Integer convertToInt = toBoundedInteger createScientificNum
So on top of the different floating and integral typeclasses, we also have the
Num typeclass. This brings them all together under one roof. The required elements of this typeclass are your basic math operators.
class Num a where (+), (*), (-) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a
This class is also somewhat analogous to the
IsString typeclass. The
IsString class let us use a string literal to represent our different types in code. In the same way, the
Num typeclass allows us to represent our value using a numeric literal. We don’t even need a compiler extension for it! This is particularly helpful when you make a "newtype" around a number. It's nice to not have to always write the constructor for it.
newtype MyNumberType = MyNumberType Int instance Num MyNumberType where -- Implement math functions using underlying int value ... myNumWithLiteral :: MyNumberType myNumWithLiteral = 5
Numbers, like string types, can be super confusing in Haskell. It’s hard to keep them straight. It’s especially difficult when you’re trying to interoperate between different types. It feels like since numeric types are so similar it should be easy to use them. And for once the type system seems to get in the way of simplicity here.
Luckily, polymorphism allows us many different avenues to fix this. We can almost always make our functions apply to lots of different numeric types. We’ll often need to convert our types to make things interoperate. But there are generally some nice functions that allow us to do this with ease.
If you’ve never programmed in Haskell before and want to try it out, you should check out our Getting Started Checklist. It will walk you through installing Haskell on your computer. It will also point you towards some tools that will help you in learning the language.
If you’ve experimented a little bit and want some extra practice, you should download our Recursion Workbook. It contains two chapters of materials as well as 10 practice problems!