In today’s Programming Praxis exercise, our goal is to implement common operations on rational numbers. Let’s get started, shall we?

We could of course just use the Data.Ratio module, which does everything we need, but that kind of defeats the purpose of the exercise.

Creating a rational number isn’t too difficult, but if you condense the various cases into a single expression like I did here you need to take care to get the signs right. Fortunately, that’s what unit testing is for. I’m using plain tuples here for the sake of brevity. In production it would be advisable to use a data type instead to prevent passing in invalid rationals such as (1,0).

ratio :: Integral a => a -> a -> (a, a)
ratio _ 0 = error "Division by zero"
ratio n d = (signum d * div n g, abs $ div d g) where g = gcd n d

For adding we use the given formula. Subtracting is the same as adding the negative of the second number.

plus :: Integral a => (a, a) -> (a, a) -> (a, a)
plus (nx, dx) (ny, dy) = ratio (nx * dy + dx * ny) (dx * dy)
minus :: Integral a => (a, a) -> (a, a) -> (a, a)
minus x (ny, dy) = plus x (-ny, dy)

Multiplication didn’t pass the unit tests at first. Turns out the formula in the Scheme solution is wrong, so I replaced it with the correct one. Division is just multiplying by the inverse of the second number.

times :: Integral a => (a, a) -> (a, a) -> (a, a)
times (nx, dx) (ny, dy) = ratio (nx * ny) (dx * dy)
divide :: Integral a => (a, a) -> (a, a) -> (a, a)
divide x (ny, dy) = times x (dy, ny)

Finally, there’s the comparison operator.

lessThan :: Integral a => (a, a) -> (a, a) -> Bool
lessThan (nx, dx) (ny, dy) = nx * dy < dx * ny

I used a decent number of unit tests to cover all of the potentially problematic cases.

main :: IO ()
main = do print $ ratio 1 2 == (1,2)
print $ ratio 1 (-2) == (-1,2)
print $ ratio (-1) 2 == (-1,2)
print $ ratio (-1) (-2) == (1,2)
print $ ratio 2 4 == (1,2)
print $ plus (1,2) (-1,6) == (1,3)
print $ minus (1,2) (1,6) == (1,3)
print $ times (3,5) (5,3) == (1,1)
print $ times (2,5) (3,7) == (6,35)
print $ times (2,5) (-3,7) == (-6,35)
print $ divide (3,4) (3,2) == (1,2)
print $ divide (1,3) (-2,3) == (-1,2)
print $ lessThan (1,3) (1,2)
print $ lessThan (-1,2) (1,6)
print $ lessThan (-1,2) (-1,6)

Everything passes, so it looks like things are working correctly.