Power Series

Consider a power series such as:

\[ \cos x = 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \frac{x^6}{6!} + …​ \]

How would you put it in a computer program?

Reading this paper shocked me. What sorcery is this?! How can a handful of succinct lines do so much? And how can a computer possibly understand them? They look like mathematical equations that only make sense to highly trained humans. I felt compelled to keep digging until I could answer these questions.

Today, I’m proud that not only do I know how it all works, but I have also written a Haskell compiler that can run McIlroy’s code on this Jupyter-esque webpage. Some changes are needed because I strayed from standard Haskell. I dislike the Num typeclass; I prefer to categorize mathematical objects like a mathematician. I have a Ring typeclass for things that can be added, subtracted, and multiplied, and there is no requirement for signum or abs, which lack meaning in some rings. Nonetheless, porting my code to standard Haskell is straightforward.

For power series, we have:

instance Ring a => Ring [a] where
  (+) = zipWith (+)
  (-) = zipWith (-)
  (f:ft) * gs@(g:gt) = f*g : map (f*) gt + ft*gs
  fromInteger = (: repeat 0) . fromInteger

How about division? Equational reasoning spits out the answer. Suppose:

f:ft = (q:qt) * gs@(g:gt)

From the definition of multiplication:

f:ft = q*g : map (q*) gt + qt * gs

Thus for nonzero g:

q = f/g
qt = (ft - map (q*) gt)/gs

This is a serviceable definition, which appears in the paper, but McIlroy later refines it:

ft = map (q*) gt + qt * gs
   = map (q*) gt + qt * gt + map (g*) qt
   = qs * gt + map (g*) qt

Hence:

qt = map (/g) (ft - qs * gt)

Adding a case for g = 0 yields the division routine:

instance (Eq a, Ring a, Field a) => Field [a] where
  (0:ft) / (0:gt) = ft/gt
  (f:ft) / (g:gt) = qs where qs = f/g : map ((1/g)*) (ft - qs*gt)

Differentiation and integration from 0 to \(x\) are easy, since \(D x^n = n x^{n-1}\).

diff (_:ft) = zipWith (*) ft [1..]
int fs = 0 : zipWith (/) fs [1..]

And then, magic.

Small is beautiful

I forget how I felt this far into the paper. I might have been unmoved: sure, some expressions may look nicer in Haskell, and maybe laziness makes some ideas easier to express, but is it really that different to other languages? However, my outlook had definitely changed by the time I’d read the next definitions.

We define sine and cosine as power series satisfying:

\[ \begin{align} D \sin x &= \cos x & \sin 0 = 0 \\ D \cos x &= -\sin x & \cos 0 = 1 \\ \end{align} \]

Equivalently:

\[ \begin{align} \sin x = \int_0^x \cos t dt \\ \cos x = 1 - \int_0^x \sin t dt \\ \end{align} \]

In Haskell:

sins = int coss
coss = 1 - int sins

take 10 sins
take 10 coss

Unlike conjuring tricks, I still experience a sense of wonder on seeing this, despite knowing the secret. Jack Rusher echoes my sentiments in his talk, Stop Writing Dead Programs:

  • "the most beautiful piece of program that I’ve ever seen"

  • "the best source code in the world"

  • "I want to cry when I look at this because of how beautiful it is"

Sadly, he then claims it has "nothing to do with software engineering". He has a point: current compilers are unable to turn these two lines into efficient machine code. I myself use CORDIC to compute trigonometric functions.

But is this because of a fundamental limit? If humans can turn elegant equations into quick calculations, surely we can teach computers to do the same. So I say we should view McIlroy’s gems as shining beacons, drawing in computer scientists to work towards systems that routinely transform beautiful mathematics into fast code.

I also claim that software engineering includes rapid prototyping, for which our code is well-suited. We can already use the few lines we’ve written so far to compute trigonometric functions:

eval (f:ft) x = f : map (x*) (eval ft x)

sum $ take 16 $ eval sins (3.141592654/6)
sum $ take 16 $ eval coss (3.141592654/4)

Anyway, back to McIlroy. The exponential function can be defined similarly to the sine and cosine:

exps = 1 + int exps

take 10 exps
fromRational $ sum $ take 10 exps :: Double

The composition of \(f(z)\) and \(g(z)\) is \(f(g(z))\), which we can think of as evaluating a power series at another power series rather than a number. Our code can only compute the answer’s constant term when \(g(0) = 0\), leading to a definition that is similar to eval:

(f:ft) # gs@(0:gt) = f : gt*(ft#gs)

The reversion or inverse of \(f(z)\) is the power series \(r(z)\) satisfying \(f(r(z)) = z\). Much has been published on this operation; Knuth dedicates 4 pages to it near the end of The Art of Computer Programming, Volume 2.

In contrast, McIlroy applies a dash of equational reasoning. From above, we must have \(r(0) = 0\), thus:

(f:ft) # rs@(0:rt) = f : rt*(ft#rs) = 0 : 1

Hence f = 0 and rt = 1 / (ft#rs):

revert (0:ft) = rs where rs = 0 : 1/(ft#rs)

Done! Let’s take it for a spin on:

\[ \tan^{-1} x = \int_0^x \frac{dt}{1 + t^2} \]

We revert to get the tangent power series:

tans = revert $ int $ 1/(1:0:1)
take 10 tans

We confirm \(\tan(x) = \sin(x) / \cos(x)\) for several terms:

take 10 $ tans - sins / coss

Next up are square roots. Suppose \((q(x))^2 = f(x)\). By the chain rule:

\[ Dq = \frac{Df}{2q} \]

If \(f(0) = 1\) then:

\[ q(x) = 1 + \int_0^x \frac{Df(t) dt}{2q(t)} \]

We generalize slightly to obtain a square root routine that works for power series whose first term has coefficient 1 and has an even degree:

sqrt1 = \case
  0:0:ft -> 0 : sqrt1 ft
  fs@(1:ft) -> qs where qs = 1 + int((diff fs)/(2*qs))

Now we can test another identity:

take 10 $ sins - sqrt1(1 - coss^2)

We can do much more with McIlroy’s code, such as solve difficult combinatorics problems.


Ben Lynn blynn@cs.stanford.edu 💡