Brandon.Si(mmons)

code / art / projects

Lazy Arithmetic in Haskell

We don’t usually give much thought to Numeric data types beyond whether we want to work with integers or decimal numbers. And that is a shame! In this post I’ll look at how we can actually do arithmetic operations lazily, and in the process hopefully reveal a bit about haskell’s numeric classes.

module LazyArithmetic
    where

We will be using Lennart Augustsson’s numbers library which can be installed from hackage with cabal-install:

$ cabal install numbers

import these libs:

import Data.Number.Natural
import Data.List(genericLength)

Consider two functions: the Prelude function sum and genericLength from the List library:

genericLength :: (Num i) => [b] -> i

sum :: (Num a) => [a] -> a

…two simple functions that have the potential to be very expensive, depending on the length of the list and the values of the elements. Say for instance that all we want is to use:

sumLengths = sum . map genericLength

checkLengths as n = (sumLengths as `div` n) > 2

The checkLengths function above has to count every element of every element of every sub-list in order to return a Bool value, and if any of those lists are infinite our program will never terminate:

hopeless = checkLengths [['a'..'z'], ['0'], repeat 'Z'] 99

…or so we might think!

Lazy Natural Numbers

It turns out we can solve our seemingly hopeless situation with a type signature:

sumLengths :: [[b]] -> Natural

And suddenly, as if by magic:

*LazyArithmetic> hopeless True

What’s going on here? Well it turns out the problem is that Haskell’s built-in numeric types are boring, old-fashioned and (in some cases) just plain wrong. But let’s step behind the curtain and look at how the Data.Numbers library defines the Natural type internally:

data Natural = Z | S Natural

We see that we’re using an abstract data type to represent non-negative integers, and it looks nearly identical to our list type []. The upshot of that is we gain, in our numeric type and arithmetic operations, the same type of laziness that lists afford us!

So in the above example, hopeless, we need only carry out the operations far enough to see that the result is > 2.

The numeric type in Data.List.Natural are actually known as Peano numbers. They are essentially just lists of units and, as you might imagine, are not the most efficient way to represent an integer.

But they are exactly what we want for this application. If we failed to realize that we could solve our problem with an appropriate numeric representation, we might try to rig up something like this:

checkLengthsUnclear as n = not $ null $ drop (n*2) $ concat as

…which works identically to checkLengths with Peano Numbers, but completely obfuscates the intention of the function. As usual laziness gives us expressive power rather than an efficiency boost.

Limitations, Haskell’s Numeric Type Classes, and the Numeric Prelude

The Data.Number.Natural library is convenient because it provides Num and Integral instances so we can use the the Natural type with any function that is polymorphic on those classes.

The problem is the Num instance is incomplete and error-prone: and that’s because a natural number type shouldn’t be an instance of Num! We can’t negate a Natural, which in turn makes subtraction dangerous, etc.

*LazyArithmetic> let x = 1 :: Natural; y = 2 in x - y 
*** Exception: Natural: (-)

We might wish that haskell’s Numeric type classes were more expressive, allowing for more abstract numeric types. For example it might be more “haskelly” for the length function to be defined with the type:

length :: Natural n => [a] -> n

After all we don’t use integers for boolean values as in C; we have the abstract type Bool. So why return an Int when a Natural type (or class) might be more expressive? The answer of course is that that gets really complicated really fast.

For one approach at making haskell’s numeric class hierarchy more expressive and flexible, check out the Numeric Prelude. It’s haddock documentation is very thorough and an interesting read, and you can see the sheer number of design decisions necessary for such an undertaking.

For some other approaches and links, see here.

Comments