“Should The Value of Pi Change”
One of my favourite anecdotes about computer programming concerns a classic defence of the recommended practice of using “named constants” to avoid the proliferation of inscrutable and hard to maintain “magic numbers” in a codebase. It goes like this:
“The primary purpose of the DATA statement is to give names to constants; instead of referring to pi as 3.141592653589793 at every appearance, the variable PI can be given that value with a DATA statement and used instead of the longer form of the constant. This also simplifies modifying the program, should the value of pi change.”
— Early FORTRAN manual for Xerox Computers, attributed to David H. Owens.
(BTW: I’m pretty sure I found an original scan of this quote many years ago but, unfortunately, the internet is now flooded with re-quotes, so I can’t find the alleged source any more. Anyway, regardless of whether it is literally true or not, it’s a great story).
I got to thinking about this stuff again in the aftermath of writing my “Buffon’s Needle” style Pi approximator yesterday for “Pi Day” 2015.
When I started writing that little “toy program”, I actually considered doing it as an exercise in “Test Driven Design”, but fairly quickly decided against that plan for a number of reasons, including these:
- I already knew the “design” that I’d be using (and it’s trivial)
- I knew it would involve randomness and graphics (both of which are potentially annoying elements for naive test automation)
- It was going to be mostly “straight line” code, lacking any of the control-flow complexity that warrants separate testing.
- Also, I knew I’d be getting lots of immediate visual feedback from the code itself as I worked on it, and that any (significant) errors (probably) couldn’t hide for very long.
- If I did have any explicit test-cases, they’d probably be quite tedious to enumerate, but the naturally intensive “random sampling” of the solution would probably cover them well enough anyway.
- I wanted to keep the code self-contained and short, and I certainly didn’t want to pull any kind of testing framework into a jsFiddle fragment.
I think the resulting non-TDD implementation came out pretty well, but I found myself feeling slightly guilty about the lack of explicit Unit Tests, and wondering what would happen if I tried to retro-fit them…
If It Ain’t Broke…
Of course, by this point, the code already existed, and it appeared to be free of obvious bugs (although I knew there could be minor subtleties regarding some of the numerical “edge-cases”), so why bother?
Well, in truth, there is no compelling reason: this piece of code is trivial, and seems unlikely to have any kind of maintenance future. On the other hand, it’s an amusing little example, and I thought it might be interesting to see how I tackled adding post-hoc tests to it, and whether those would make it in any sense “better” e.g. more self-documenting and understandable.
…Fix It Anyway
My first thought was to “factor out” the randomness and the direct graphics rendering from the central throwPin function. To do that, I’d have to explicitly pass the “random” position and angle of each pin as inputs and, instead of letting it draw the lines directly, I’d have to return their coordinates and draw (or, for testing, check) them separately.
This didn’t seem like too much pain, but it quickly raised a question about how exactly to feed-in those random inputs: those positions need a bit of scaling at some point, but if I pulled the necessary scaling logic outside of the throwPin function, I felt I’d be spreading-out some rather messy “implementation entrails” into other parts of the code. I decided that the best thing was to feed-in raw random values in a standard “normalised” range of zero to one, and keep the scaling inside the function under test.
However, the input “angle” was a bit of a special case. To provide a correct sampling, it should really be based on a range of 0 to 2*Pi (radians), but I didn’t want the value of Pi to appear inside any of the supposed “Pi Calculating” machinery of the code. So, in this case, I actually decided to pull the scaling outside of the target function instead.
The result of all this was that I created a “pure functional” core called: throwPin_core(xNormed, yNormed, angle)
To begin with, I rescaled the “normed” parameter values internally, using the old LEFT_BAR and BAR_SEP “constants”, but then something interesting happened: I realised that I didn’t actually need them!
…It was perfectly possible to make the core routine work entirely in a normalised space, and to only do the scaling work on the outside — specifically, in the (extracted) rendering logic. That felt good, and immediately seemed like an attractive “separation of concerns” where, previously, there had been some slightly messy conflation.
The resulting clean-up also exposed the fact that I had been cheating a bit with my y-scaling: now, this certainly wasn’t a bug, but I had been giving the y-values of the pins a slightly different scaling range than the x-values (essentially for “aesthetic” reasons), and I realised that the code was cleaner (and the aesthetics “unharmed”) if I unified the scales and adopted an obviously “square” drop-zone for my pins instead.
Writing Unit Tests
The extraction of a testable, functional, core was a pre-requisite to writing any actual tests — but it had already shown me some benefits simply by encouraging a more “decoupled” design. But would actually writing some explicit tests provide any further improvement?
One of my first problems was deciding how thorough and explicit to be in this testing. To be honest, I felt that I already had a fairly persuasive “integration test” in the form of being able to run the damn thing and get something pretty close to the right answer :-) — and I didn’t want to tediously enumerate a ton of minor test instances just to prove that I had “covered all the angles” here (almost literally).
So, what I did was possibly a bit naughty: I just threw together a modest number of “partial tests”, mostly concentrating on the edge-cases of the problem (which seem like the only real fragile or subtle points in this exercise).
I decided that I didn’t want to independently compute the correct coordinates for an extensive set of potential pins (which could only be a stupidly sparse sampling of the whole anyway), so I actually chose to ignore the resulting output coordinates, and just focused on testing the boolean hit-testing and “color-coding” logic (yes, I use the American spelling when programming, to match standard API conventions).
That led me to a series of simple test-cases like this:
throwPin_expect(“right-limit pos, vertical”, 0.999, y, Math.PI/2, false, “green”);
I also added a layer which “multiplied” each test, by using all 4 symmetrical variants of the input angle (i.e. mirroring in both axes), which gives more test-confidence / coverage over the geometry calculations inside the implementation.
Having to “roll my own” test framework was a bit of a pain, so I kept it very simple, and did just enough to provide basic “this test broke” feedback at runtime. I resisted embellishment.
I was relatively happy with this effort (despite it being an essentially pointless one in many, many, ways!), and was impressed to see it actually improving the core of the code… I think.
You can find the updated version HERE.
The tests themselves add a bit of “bloat” of course, but I pushed them to the bottom of the file where they don’t get in the way. They hopefully serve as a kind of (limited) documentation of my intent, especially around the edge-cases, and might serve as a little bit of a safety-net if anyone were to attempt tweaking this code again in future…
…although, of course, WHY WOULD THEY?
Unless the value of Pi changed!
Disclaimer / Excuse
I’m pretty tired, and I arguably wimped-out of going the whole hog with this analysis, so it’s incomplete and certainly not rigorous. I need to move on with my life now though, so it’s a case of either “publish and be damned” or “throw it away”. (Have I chosen correctly, I wonder?)
If I was trying to be “textbook” about it, I would probably say more about checking the numerical details, and might even attempt to measure the distribution of the underlying randomness function (ha!).
Knowing my luck, there will turn out to be some kind of annoying stupid bug in this code, which my sparse testing will have left open. If so, and you find it, I’ll endeavour to fix it in time for next Pi Day.
Thanks for reading.