Saturday, October 25, 2014

Do it bottom up in the REPL!

Today I stumbled over a question on StackOverflow, where a poster faced difficulties with some homework. The task was to compute pi with the approximation

pi = 3.0 + 4 / (2*3*4) - 4 / (4*5*6) + 4 / (6*7*8) - ...
and he was getting nonsesical results. It followed 35 lines of Java code (not counting empty lines) and the request for help "because it must be submitted on tuesday".

Doing it the conventional way

It occured to me that Java lures beginners into writing some monolithical main method, and when it doesn't work, debugging begins.

Debugging is in some sense the dual to programming: in imperative programming, we assemble expressions and statements to a whole, the glue is, of course, mutable state. And the debugger is a tool to help you to do the reverse: you split that whole to be able to look at individual statements and expressions.

Doing it the functional way

But how if we could spare us this assembling and disassembling, and instead do it right from the start? This is, of course, not a new idea, and is well known by the name "bottom up". I am, for my part, strongly convinced that this is how to think when working in functional languages.

In praise of the REPL

For this, we need another tool, namely an interpreter, also called REPL (for read-eval-print-loop). As if to underline my theory, there is no REPL in Java (or, at least, it is very uncommon to use one), Frege has no debugger at all, whereas in Haskell, ironically, setting breakpoints is a feature of the interpreter ghci.

Ask a person whose favorite language you don't know to choose two out of compiler, interpreter and debugger. If the answer is "compiler and debugger" you can safely bet on C++, Java or C. If, however, the debugger is the least important tool, you found a functional or declarative programmer.

In the following, I'll work through the example using the Frege REPL, in the hope to show some reader with an imperative mindset how to "go about it" in a functional way. 

Going to the bottom

But before we can go "bottom up", we need to go "top down" mentally. At first, one needs to see that we have to compute a sum whose terms have a nice pattern (except the leading 3.0, of course). The key point is the smallest number in the products that serve to divide 4. Hence, we obviously need the positive even numbers, since the whole thing is based on them.

Going bottom up

We couldn't go deeper anymore for the moment, because we can write this directly in Frege:

frege> [2,4..]
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56

Thanks to foresight of the REPL author, Marimuthu Madasamy, the REPL will print only an initial part of an infinite list, so we won't get kicked out at this point!
Now that we have that list of integers, it is time to remember that we will need floating point numbers instead, because Frege won't let us mix integers and reals in numerical computations. Nothing easier than that! Get back that previous line and change it accordingly:

frege> map Int.double [2,4..]
[2.0,4.0,6.0,8.0,10.0,12.0,14.0,16.0,18.0,20.0,22.0,24.0,26.0,28.0,30.0,32.0,34.

In principle, we could proceed in this way and modify the last expression further and further, until we arrive at the solution. It is, however, recommended to name important sub-solutions. I usually do this when the expression gets too complex, makes sense on its own or if it is a list I do not want to get recomputed always.

frege> evens = map Int.double [2,4..]
value evens :: [Double]

The friendly type checker ensures us that we have a list of Doubles, as was our intention.
Time to write the code for the terms of the sum:

frege> [ 4 / n*(n+1)*(n+2) | n <- evens ]
[24.0,30.0,37.33333333333333,45.0,52.800000000000004,60.666666666666664,68.57142

Surprise! This result is surely not what we want, as the terms should get smaller and smaller instead of bigger and bigger!
It turns out that the whole product should be in the divisor, not just the first term, so we just forgot a pair of parentheses.

But note how such an error would escape us in a compile-only setting until runtime. The bottom up method already pays off!

frege> [ 4 / (n*(n+1)*(n+2)) | n <- evens ]
[0.16666666666666666,0.03333333333333333,0.011904761904761904,0.0055555555555555

Doesn't this look much better?
Now, we know that every second terms needs to be negative. If we only could apply negate to every other term! But wait, we can, of course:

frege> deltas = zipWith ($) (cycle [id,negate]) [4 / (n*(n+1)*(n+2)) | n <- evens]
value deltas :: [Double]

And this is it, essentially! We can now directly write a function that gives us pi, approximated with so many terms:


frege> p n = 3.0 + sum (take n deltas)
function p :: Int -> Double
frege> map p [0..10]
[3.0,3.1666666666666665,3.1333333333333333,3.145238095238095,3.1396825396825396,
frege> map p [20..40]
[3.1415657346585473,3.1416160719181865,3.1415721544829647,3.1416106990404735,3.1
frege> map p [1000]
[3.1415926533405423]
frege> map p [10000]
[3.1415926535895418]
frege> import Prelude.Floating
frege> Double.pi
3.141592653589793
frege> Double.pi - p 10000
2.5135449277513544E-13

Why wait until tuesday? We can submit monday.

6 comments:

  1. I would like to do it left to right as well.

    ReplyDelete
  2. Great post Ingo! I personally consider that guidelines such as the ones you just provided in this post are so important for people to go in the right way. This is especially true for people who had their brain conditioned for years to think differently by working in a non-functional (e.g. OO) setting.

    ReplyDelete
  3. Great post..Thanks for sharing this article..
    Shoofi

    ReplyDelete
  4. Good information.I like the way of writing and presenting.The author clearly describe all the parts of the topic.I bookmarked this post.Eagerly waiting for your next post.custom essay writing service

    ReplyDelete
  5. This is really a best share,.
    i like it,.
    angularjs

    ReplyDelete

Comments will be censored by me as I see fit, most likely if they contain insults or propaganda for ideologies I do not like. Comments that are on topic will not be censored. If I leave a comment uncensored this does not imply that I agree with the opinions expressed therein.