Addition Chains as Polymorphic Higher-order Functions

Previously in An Introduction & Supplement to Knuth's Introduction to Addition Chains we developed the AdditionChainConstruction module. Now we're going to develop the AdditionChainComputation module, which does the same things, but differently. AdditionChainConstruction requires us to construct explicit tree structures that then must be traversed by functions to calculate results. Yet an addition chain itself is a sort of a traversal. The structure of an addition chain can be encoded as a polymorphic function that acts as a fold over the doubles and (non-doubling) adds in the chain.

The new code is the module AdditionChainComputation in the file AdditionChainComputation.lhs. Once again, we'll avoid any language extensions (that aren't enabled by default in GHCi 8.0.1) and we'll only use library functions from the base package. If you compare this code to the previous code, you'll see that it is even simpler in that it doesn't even (directly) use type classes. This code is also more efficient; the denote function in this new module can easily handle large inputs that cause AdditionChainConstruction to choke.

We define an AdditionChain to be a function that, given double and add functions as arguments, along with a representation of the value one, evaluates double and add according to the structure of the addition chain, starting with the value one. All details of the concrete representation of values are completely abstracted away.

Measuring addition chains in AdditionChainComputation is as tedious as it is in AdditionChainConstruction. Here, df constructs a list of (a, b, x) tuples where a + b == x. Then it filters out any and all duplicates. We'll come back to the topic of how awkward this is in a bit. Before that, let's define the remaining functions.

λ (a.k.a. lambda), v, and bitString are defined similarly to their counterparts in AdditionChainConstruction, but they they'll be defined on Naturals only. This means we'll have to write λ (denote n), v (denote n), and bitString (denote n) where previously we wrote λ n, v n, and bitString n, respectively. We'll add a hexString function that wasn't in the previous module to make up for the decreased convenience.

Let's go back to the awkwardness of the df function. The way in which we encounter an entry in an addition chain multiple times and have to filter out the duplicates is counter to the whole point of addition chains, which is to compute/visit each value in the chain once, in order, efficiently. Basically addition chains memoize computations, yet we're not memoizing anything. That means that this new way of dealing with addition chains doesn't really model their essence.

AdditionChainConstruction's denote function can't handle addition chains for very large values due to this lack of memoization (AFAICT). Its counting functions (d, f, r, etc.) don't have such trouble. When I wrote that code I had expected its denote to work more like those other functions. The new AdditionChainComputation module's denotecan handle long addition chains, as can all of its other functions, so we can defer the work of implementing memoization to another day.

As we did with smaller values before, we can check denote bigNumber == 2^255 - 19 - 2 to verify that this addition chain really represents 2255 - 19 - 2. It's also instructive to actually look at the number in hexadecimal and binary notation.