Here's a new Haskell **WAT?!**

Haskell has a type `Rational`

for working with precisely-valued fractional numbers, and it models the mathematical concept of a rational number. Although it's relatively slow compared with `Double`

, it doesn't suffer from the rounding that's intrinsic to floating-point arithmetic. It's very useful when writing tests because an exact result can be predicted ahead of time. For example, a computation that should produce zero will produce exactly zero rather than a small value within some range that would have to be determined.

`Rational`

is actually a (monomorphic) specialization of the more general (polymorphic) type `Ratio`

(from `Data.Ratio`

). `Ratio`

allows you to specify the underlying type used for the numerator and denominator. For example, to work with rational numbers using `Int`

as the underlying type you can use `Ratio Int`

. For the common case of using `Integer`

as the underlying type, the type synonym `Rational`

is provided:

```
type Rational = Ratio Integer
```

It's tempting to use `Ratio`

with a fixed-width type like `Int`

because `Int`

is much faster than `Integer`

. However, let's see what can happen if you do this:

```
λ> import Data.Int
λ> import Data.Ratio
λ> let r = 1 % 12 :: Rational in r - r == 0
True
λ> let r = 1 % 12 :: Ratio Int8 in r - r == 0
False
```

**WAT?!**

Let's see what those subtracted values evaluate to:

```
λ> let r = 1 % 12 :: Rational in r - r
0 % 1
λ> let r = 1 % 12 :: Ratio Int8 in r - r
0 % (-1)
```

Hmmm, let's see if that `Ratio Int8`

value is considered equal to `0`

:

```
λ> let r = 0 % (-1) :: Ratio Int8 in r == 0
True
```

**WAT?!**

Let's see what those manually-entered values are:

```
λ> 0 % (-1) :: Ratio Int8
0 % 1
λ> 0 :: Ratio Int8
0 % 1
```

OK, so these values really are equal, but why are the values in the subtraction different? The explanation is two-fold.

First, `0 % (-1)`

is a denormalized state for `Ratio`

and shouldn't occur. (As you've probably suspected, it arises from integer overflow. More on that in a minute.) It's not too surprising, then, that it isn't equal to `0`

.

But why is it equal to `0`

when we enter it directly? It's because `%`

is a function not a constructor, and it normalizes the signs of the numerator and denominator before constructing the value:

```
x % y = reduce (x * signum y) (abs y)
```

The underlying assumption (the *invariant*) is that denominators will always be positive.

`reduce`

is a function that reduces the numerator and denominator to their lowest terms, by dividing by the greatest common divisor:

```
reduce x y = (x `quot` d) :% (y `quot` d)
where d = gcd x y
```

Here you can see the constructor that actually creates the values from their components, which is `:%`

. It's not exported from `Data.Ratio`

and the "smart constructor" `%`

is used instead, to ensure that new `Ratio`

values always satisfy the invariant.

Second, addition and subtraction are implemented without trying to minimize the possibility of integer overflow. For example:

```
(x :% y) - (x' :% y') = reduce (x * y' - x' * y) (y * y')
```

If `y * y'`

overflows to a negative value, `reduce`

will not normalize the signs. The result of `gcd`

is always non-negative so the signs don't change and denormalized values are never renormalized. That happens only in `%`

when constructing `Ratio`

values.

Let's look at what happens in our example:

```
λ> x = 1; y = 12; x' = 1; y' = 12
λ> x * y' - x' * y :: Int8
0
λ> y * y' :: Int8
-112
λ> gcd 0 (-112)
112
λ> 0 `quot` 112
0
λ> (-112) `quot` 112
-1
```

The reduced result of `1 % 12 - 1 % 12`

is therefore the denormalized value `0 :% (-1)`

which isn't considered equal to the normalized value `0 % 1`

.

Even though `12`

is much less than `maxBound :: Int8`

, when squared it results in integer overflow. The implementation of `Num`

for `Ratio`

is *not* designed to avoid overflows and they can happen very easily with numerators and denominators that are much less than the `maxBound`

for the type.

The implementation could have used a slightly different approach:

```
(x :% y) - (x' :% y') = reduce (x * z' - x' * z) (y * z')
where z = y `quot` d
z' = y' `quot` d
d = gcd y y'
```

However, the use of `reduce`

is still necessary (consider `3 % 10 - 2 % 15`

) so this requires two more divisions and a `gcd`

compared with the actual implementation.

Using a type as small as `Int8`

might seem a little unrealistic, but the problem can occur with any fixed-width integral type and I used `Int8`

for the illustration because it's easier to understand the problem when working with small values. I originally encountered it when using `Ratio Int`

even though `Int`

has a very large `maxBound`

. I was writing property tests using QuickCheck for some polymorphic arithmetic code that was supposed to produce a zero sum as a result. The test succeeded with `Rational`

and failed with `Ratio Int`

and I couldn't understand why because the random values being generated by the test framework had numerators and denominators far less than `maxBound :: Int`

. However, they *were* greater than its square root.

The documentation for `Ratio`

says:

Note that Ratio's instances inherit the deficiencies from the type parameter's. For example,

`Ratio Natural`

's`Num`

instance has similar problems to`Natural`

's.

However, that doesn't really prepare you for what might happen with other type parameters! The moral of this story is that `Ratio`

isn't much use on its own and you should always use `Rational`

unless you *really* understand what you're getting into.

Like that blog post? Check out the Haskell section of our site with tutorials and other blog posts. You can also check out all Haskell tagged blog posts.

**We're hiring.** Interested in working with our team on solving these kinds of WAT issues? Check out our jobs page for more information.

Subscribe to our blog via email

Email subscriptions come from our Atom feed and are handled by Blogtrottr. You will only receive notifications of blog posts, and can unsubscribe any time.

Do you like this blog post and need help with Next Generation Software Engineering, DevSecOps or Blockchain & Smart Contracts? Contact us.