Unbounded Spigot Algorithms for the Digits of Pi
Total Page:16
File Type:pdf, Size:1020Kb
Unbounded Spigot Algorithms for the Digits of Pi Jeremy Gibbons 1 INTRODUCTION. Rabinowitz and Wagon [8] present a “remarkable” algorithm for com- puting the decimal digits of π, based on the expansion ∞ (i!)22i+1 π = ∑ . (1) i=0 (2i + 1)! Their algorithm uses only bounded integer arithmetic, and is surprisingly efficient. Moreover, it admits extremely concise implementations. Witness, for example, the following (deliberately obfuscated) C pro- gram due to Dik Winter and Achim Flammenkamp [1, p. 37], which produces the first 15,000 decimal digits of π: a[52514],b,c=52514,d,e,f=1e4,g,h;main(){for(;b=c-=14;h=printf("%04d", e+d/f))for(e=d%=f;g=--b*2;d/=g)d=d*b+f*(h?a[b]:f/5),a[b]=d%--g;} Rabinowitz and Wagon call their algorithm a spigot algorithm, because it yields digits incrementally and does not reuse digits after they have been computed. The digits drip out one by one, as if from a leaky tap. In contrast, most algorithms for computing the digits of π execute inscrutably, delivering no output until the whole computation is completed. However, the Rabinowitz–Wagon algorithm has its weaknesses. In particular, the computation is in- herently bounded: one has to commit in advance to computing a certain number of digits. Based on this commitment, the computation proceeds on an appropriate finite prefix of the infinite series (1). In fact, it is essentially impossible to determine in advance how big that finite prefix should be for a given number of digits—specifically, a computation that terminates with nines for the last few digits of the output is inconclusive, because there may be a “carry” from the first few truncated terms. Rabinowitz and Wagon suggest that “in practice, one might ask for, say, six extra digits, reducing the odds of this problem to one in a million” [8, p. 197], a not entirely satisfactory recommendation. Indeed, the implementation printed at the end of their paper is not quite right [1, p. 82], sometimes printing an incorrect last digit because the finite approximation of the infinite series is one term too short. We propose a different algorithm, based on the same series (1) for π but avoiding these problems. We also show the same technique applied to other characterizations of π. No commitment need be made in advance to the number of digits to be computed; given enough memory, the programs will generate digits ad infinitum. Once more (necessarily, in fact, given the previous property), the programs are spigot algorithms in Rabinowitz and Wagon’s sense: they yield digits incrementally and do not reuse them after producing them. Of course, no algorithm using a bounded amount of memory can generate a nonrepeat- ing sequence such as the digits of π indefinitely, so we have to allow arbitrary-precision arithmetic, or some other manifestation of dynamic memory allocation. Like Rabinowitz and Wagon’s algorithm, our proposals are not competitive with state-of-the-art arithmetic-geometric mean algorithms for computing π Copyright the Mathematical Association of America 2005. All rights reserved. 1 [2], [9]. Nevertheless, our algorithms are simple to understand and admit almost as concise an implemen- tation. As evidence to support the second claim, here is a (deliberately obscure) program that will generate as many digits of π as memory will allow: > pi = g(1,0,1,1,3,3) where > g(q,r,t,k,n,l) = if 4*q+r-t<n*t > then n : g(10*q,10*(r-n*t),t,k,div(10*(3*q+r))t-10*n,l) > else g(q*k,(2*q+r)*l,t*l,k+1,div(q*(7*k+2)+r*l)(t*l),l+2) The remainder of this paper provides a justification for the foregoing program, and some others like it. These algorithms exhibit a pattern that we call streaming [3]. Informally, a streaming algorithm con- sumes a (potentially infinite) sequence of inputs and generates a (possibly infinite) sequence of outputs, maintaining some state as it goes. Based on the current state, at each step there is a choice between producing an element of the output and consuming an element of the input. Streaming seems to be a com- mon pattern for various kinds of representation changers, including several data compression and number conversion algorithms. The program under discussion is written in Haskell [5], a lazy functional programming language. As a secondary point of this paper, we hope to convince the reader that such languages are excellent vehicles for expressing mathematical computations, certainly when compared with other general-purpose programming languages such as Java, C, and Pascal, and arguably even when compared with computer algebra systems such as Mathematica. In particular, a lazy language allows direct computations with infinite data structures, which require some kind of indirect representation in most other languages. The Haskell program presented earlier has been compressed to compete with the C program for conciseness, so we do not argue that this particular one is easy to follow—but we do claim that the later Haskell programs are. 2 LAZY FUNCTIONAL PROGRAMMING IN HASKELL. To aid the reader’s understanding, we start with a brief (and necessarily incomplete) description of the concepts of functional programming (henceforth FP) and laziness and their manifestation in Haskell [5], the de facto standard lazy FP language. Further resources, including pointers to tutorials and free implementations for many platforms, can be found at the Haskell website [6]. FP is programming with expressions rather than statements. Everything is a value, and there are no assignments or other state-changing commands. Therefore, a pure FP language is referentially transparent: an expression may always be substituted for one with an equal value, without changing the meaning of the surrounding context. This makes reasoning in FP languages just like reasoning in high-school algebra. Here is a simple Haskell program: > square :: Integer -> Integer > square x = x * x Program text is marked with a “>” in the left-hand column, and comments are unmarked. This is a simple form of literate programming, in which the emphasis is placed on making the program easy for people rather than computers to read. Code and documentation are freely interspersed; indeed, the manuscript for this article is simultaneously an executable Haskell program. The first line in the square program is a type declaration; the symbol “::” should be read “has type.” Thus, square has type Integer -> Integer, that is, it is a function from Integers to Integers. (Type declarations in Haskell are nearly always optional, because they can be inferred, but we often specify them anyway for clarity.) The second line gives a definition, as an equation: in any context, a subexpression of the form square x for any x may be replaced safely by x * x. 2 Lists are central to FP. Haskell uses square brackets for lists, so [1,2,3] has three elements, and [] is the empty list. The operator “:” prefixes an element; so [1,2,3] = 1 : (2 : (3 : [])). For any type a, there is a corresponding type [a] of lists with elements drawn from type a, so, for exam- ple, [1,2,3] :: [Integer]. Haskell has a very convenient list comprehension notation, analogous to set comprehensions; for instance, [ square x | x <- [1,2,3] ] denotes the list of squares [1,4,9]. For the “substitution of equals for equals” property to be universally valid, it is important not to evaluate expressions unless their values are needed. For example, consider the following Haskell program: > three :: Integer -> Integer > three x = 3 > nonsense :: Integer > nonsense = 1 + nonsense The first definition is of a function that ignores its argument x and always returns the integer 3; the second is of a value of type Integer, but one whose evaluation never terminates. For substitutivity to hold, and in particular for three nonsense to evaluate to 3 as the equation suggests, it is important not to evaluate the function argument nonsense in the function application three nonsense. With lazy evaluation, in which evaluation is demand-driven, no expression is evaluated unless and until its value is needed to make further progress. A useful by-product of lazy evaluation is the ability to handle infinite data structures: they are evaluated only as far as is necessary. We illustrate this with a definition of the infinite sequence of Fibonacci numbers: > fibs :: [Integer] > fibs = f (0,1) where f (a,b) = a : f (b,a+b) (Here, (0,1) is a pair of Integers, and the where clause introduces a local definition.) Evaluating fibs never terminates, of course, but computing a finite prefix of it does. For example, with > take :: Integer -> [Integer] -> [Integer] > take 0 xs = [] > take (n+1) [] = [] > take (n+1) (x:xs) = x : take n xs (so take takes two arguments, an Integer and a [Integer], and returns another [Integer]), we have take 10 fibs = [0,1,1,2,3,5,8,13,21,34], which terminates normally. Functions may be polymorphic, defined for arbitrary types. Thus, the function take in fact works for lists of any element type, since elements are merely copied and not further analyzed. The most general type assignable to take is Integer -> [a] -> [a] for an arbitrary type a. Because this is FP, functions are “first-class citizens” of the language, with all the rights of any other type. Among other things, they may be passed as arguments to and returned as results from higher-order functions. For example, with the definition > map :: (a->b) -> [a] -> [b] > map f [] = [] > map f (x:xs) = f x : map f xs the list comprehension [ square x | x <- [1,2,3] ] is equal to map square [1,2,3].