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.
This is another one of those things with Haskell that can definitely throw beginners for a loop. It can even make them say, “ugh, this language sucks, I’m going back to Javascript” (where numbers are easier). But there are a few simple rules you have to get under your belt, and then things become much better. Let’s start by summarizing the different numeric types we can use.
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 Int8
, Int16
, Int32
, and Int64
. They allow you to have more definite bounds on your number, while giving you the same basic functionality as Int
. Obviously 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.
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 div
and mod
functions are like quot
and rem
, but round towards negative infinity instead of 0. It can be a little constricting to have to jump through these hoops. This is especially the case when your javascript friends can just get away with always writing 5 / 2
. But that’s the price of a strong typed system.
Of course, we also have a couple different types to represent floating point numbers. Haskell has two main floating point types: Float
and Double
. The 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 -> Doubledouble2Float :: 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.
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 ceiling
, round
, 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 round
, floor
and 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 -> bround :: (Fractional a, Integral b) => a -> bfloor :: (Fractional a, Integral b) => a -> bceiling :: (Fractional a, Integral b) => a -> btoInteger :: (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 70000createScientificNum :: ScientificcreateScientificNum = scientific 7 4
-- Conversion to float (i.e. 70000.0)convertToFloat :: DoubleconvertToFloat = toBoundedRealFloat createScientificNum
-- Convert to integer, might fail if the scientific is not an integerconvertToInt :: Maybe IntegerconvertToInt = 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 :: MyNumberTypemyNumWithLiteral = 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!
Above all, be sure to visit the Monday Morning Haskell blog for more awesome Haskell content, with a new article every Monday!
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!