Programming involves a really upsetting amount of numbers. We are perpetually finding ourselves dealing with numbers in the sorts of programs that seemingly have no just cause to expose us to arithmetic tedium. The greatest redeeming quality of numbers is the sense of possibility conveyed by the unlimited expanse of the number line – But machines mute even this glimmer of hope, offering instead a dim and claustrophobic view.
Here’s the thing: We treat our numbers in a most cruel manner, packing them into dark corners, shoving them through nanoscopically thin wires. They live in tiny boxes, because in computing, small is fast. And under these conditions, our numbers cannot handle much stress before they start bouncing off the walls.
Try starting with an Int64
of 100 and repeatedly square it. You won’t get far.See Introduction to GHCi for a discussion of it
.
λ> import Data.Int
λ> 100 :: Int64
100
λ> it ^ 2
10000
λ> it ^ 2
100000000
λ> it ^ 2
10000000000000000
λ> it ^ 2
-8814407033341083648
Int64
is not a type for unbounded possibilities, not a realm in which to explore the vast glory of mathematics. Int64
offers efficient and uninspiring service for tasks such as… counting.
λ> 1 :: Int64
1
λ> it + 1
2
λ> it + 1
3
λ> it + 1
4
λ> it + 1
5
For this sort of thing, Int64
will pretty much stay in its lane. It takes a while to plus-one your way out of an Int64
box.
We do have an unbounded type! It’s called Integer
.Mnemonic: The type with the longer name can hold bigger numbers. Just as ‘Int’ is an abbrevation for ‘Integer’, types with ‘Int’ in their names are “abbreviated” notions of integerness. The Integer
type will neither give you up nor let you down.
λ> 100 :: Integer
100
λ> it ^ 2
10000
λ> it ^ 2
100000000
λ> it ^ 2
10000000000000000
λ> it ^ 2
10000000000000000000000000
0000000
λ> it ^ 2
10000000000000000000000000
00000000000000000000000000
0000000000000
λ> it ^ 2
10000000000000000000000000
00000000000000000000000000
00000000000000000000000000
00000000000000000000000000
0000000000000000000000000
It’s so nice, it makes you want to move to Integer Land, buy a little house there, plant some fruit trees, spend your evenings on the porch with a glass of wine looking out over the –
Snap out of it, you’re daydreaming! You’re not going to get Integers all the time, it’s just not realistic. In fact, it’s only going to get worse.
Do you know what the upper bound on Int64
is? We said it was small, but that’s only in comparison to infinity. Really, it’s kind of an absurdly high number.
λ> maxBound :: Int64
9223372036854775807
A lot of situations that call for a number are situations where we know that the number won’t be nearly that high. Say you’ve got a cheap digital room thermometer that reports temperatures in whole degrees Fahrenheit. Maybe it’s going to give you an Int16
. That’s way smaller than Int64
, but this bad boy can still fit so many temperatures.
λ> minBound :: Int16
-32768
λ> maxBound :: Int16
32767
Programmers, hardware builders, network protocol designers – they’re all going to give you numbers, and they’re all going to require numbers from you. And here’s the snag: Because everybody is putting their numbers into the smallest box they can get away with, and because how small a box you can get away with depends on exactly what you’re doing, sooner or later you’re going to end up with a square peg and a round hole.
“We gotta find a way to make this fit into the hole for this” – from Apollo 13 (1995)
It is astonishing how casually we often see numbers abused as they are shoved from one box to another. The fromIntegral
function in the Prelude
module is one example of how numbers can get hurt.fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b
This is an extremely polymorphic function – Both its input and output are type variables. So we’re going to need some type annotations in our demonstration, to ensure that the compiler knows what types we’re converting to and from.
Here’s how it works on a good day:
λ> fromIntegral (1243 :: Int64) :: Int16
1243
A number goes in, the same number comes out in new clothes as a different type. But what happens when the number doesn’t fit into its new clothes? Nothing good, I’ll tell you.
λ> fromIntegral (475891 :: Int64) :: Int16
17139
λ> fromIntegral (-1243 :: Int64) :: Word64
18446744073709550373
Don’t treat your numbers like this!
Generally the best tool for this kind of work is a function called toIntegralSized
.
toIntegralSized ::
Integral a, Bits a,
( Integral b, Bits b ) =>
-> Maybe b a
This function comes from a rather obscure place at the bottom of the Data.Bits
Data.Bits
module in the base
package. You can alsoRelude.Numeric
find it in the relude
package, where it is included in the Relude
module and featured more prominently in the Relude.Numeric
module.
λ> toIntegralSized (1243 :: Int64) :: Maybe Int16
Just 1243
λ> toIntegralSized (475891 :: Int64) :: Maybe Int16
Nothing
The output type is Maybe
because a reasonable conversion from one type of number to another is not always possible, owing to each type’s different boundaries. As the example above illustrates, if an Int64
value is too large, an attempt to transfer it into a smaller Int16
box may yield Nothing
.
Likewise, a negative integer will fail to convert to any “word” type; such things are only for non-negative numbers.Data.Word
λ> import Data.Word
λ> toIntegralSized (1243 :: Int64) :: Maybe Word64
Just 1243
λ> toIntegralSized (-1243 :: Int64) :: Maybe Word64
Nothing
With toIntegralSized
at your disposal, you can confidently tackle those pesky integer conversion tasks and shift your numbers around into whatever format is required of them. It doesn’t remove all your need to think, because you still have that Nothing
possibility to deal with, but you have an extremely versatile conversion function that never produces erroneous off-the-wall results.