A fold is a combination of:
x
in some sequence of xs
into the accumulating stateNOTE: This also happens to be Clojure's required interface for the reducing function you pass to [[clojure.core/transduce]]. Any of the folds implemented in this namespace work well with
transduce
out of the box.
Here is a simple example of a fold:
The accumulator is the floating point 0.0. The present
function is just... identity. This might seem pedantic to include, but many interesting folds need to keep intermediate state around, so please indulge me for now.
To "fold" a new number into the running total, simply add them together.
Here is how to use this function to add up the integers from 0 to 9:
To see how this abstraction is useful, let's first capture this ability to make "summation" functions out of folds. (Note the docstring's description of the other arities of fold->sum-fn
... these allow you to define each of the arities of a fold in separate functions if you like.)
Our example again:
This pattern is quite general. Here is example of a fold that (inefficiently) computes the average of a sequence of numbers:
The average of Loading... is 4.5:
(I'm not committing this particular implementation because it can overflow for large numbers. There is a better implementation in Algebird, used in AveragedValue
that you should port when this becomes important.)
Here are some more building blocks:
NOTE also that any [[emmy.util.aggregate/monoid]] instance will work as a fold out of the box. [[emmy.util.aggregate]] has utilities for generating more explicit folds out of monoids.
Folds can be "added" together in the following sense; if I have a sequence of folds, I can run them in parallel across some sequence xs
by combining them into a single fold with these properties:
x
is merged into each accumulator using the appropriate foldpresent
is called for every entry in the final vectorThis function is called join
:
For example, the following snippet computes the minimum, maximum and sum of (range 10)
:
Before moving on, let's pause and implement a similar transformation of a fold, called fold->scan-fn
. This is a generic form of Clojure's reductions
function; a "scan" takes a sequence of xs
and returns a sequence of all intermediate results seen by the accumulator, all passed through present
.
Here is the previous example, using the fold to scan across (range 4)
. Each vector in the returned (lazy) sequence is the minimum, maximum and running total seen up to that point.
This is a fascinating topic, and my explorations have not yet done it justice. This section implements a number of folds designed to sum up sequences of numbers in various error-limiting ways.
Much of the numerical physics simulation code in the library (everything in [[emmy.numerical.quadrature]], for example) depends on the ability to sum sequences of floating point numbers without the accumulation of error due to the machine representation of the number.
Here is the naive way to add up a list of numbers:
Simple! But watch it "break":
Algorithms called 'compensated summation' algorithms are one way around this problem. Instead of simply accumulating the numbers as they come, compensated summation keeps track of the piece of the sum that would get erased from the sum due to lack of precision.
Aggregation algorithms that require intermediate state as they traverse a sequence are often excellent matches for the "fold" abstraction.
The fold implementing Kahan summation tracks two pieces of state:
If you add a very small to a very large number, the small number will lose bits. If you then SUBTRACT a large number and get back down to the original small number's range, the compensation term can recover those lost bits for you.
See the 'Accuracy' section of the wiki article for a detailed discussion on the error bounds you can expect with Kahan summation. I haven't grokked this yet, so please open a PR with more exposition once you get it.
Voila, using [[kahan]], our example from before now correctly sums to 1.0:
From the wiki page, "Neumaier introduced an improved version of Kahan algorithm, which he calls an 'improved Kahan–Babuška algorithm, which also covers the case when the next term to be added is larger in absolute value than the running sum, effectively swapping the role of what is large and what is small."
Here is an example of where Kahan fails. The following should be 2.0, but Kahan returns 0.0:
This improved fold is implemented here:
[[kbn]] returns the correct result for the example above:
The wiki page mentions a "higher-order modification" of [[kahan-babushka-neumaier]], and I couldn't help implementing the second-order version here:
Now, the repetition above in the second-order version was too glaring to ignore. Clearly it is possible to write efficient code for as high an order as you'd like, as described in Klein's paper.
Because this code needs to be very efficient, I chose to implement the higher-order fold generator using a macro.
Each new order stacks three entries into the let-binding of the function above, and adds a new term to the accumulator. Because all of these terms live inside a single let-binding, we have to be careful with variable names. It turns out we can get away with
one symbol for each term we're accumulating (sum
and each compensation term)
a single symbol delta
that we can reuse for all deltas generated.